httpserver_tests.cpp raw

   1  // Copyright (c) 2012-present The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <httpserver.h>
   6  #include <rpc/protocol.h>
   7  #include <test/util/common.h>
   8  #include <test/util/logging.h>
   9  #include <test/util/setup_common.h>
  10  #include <util/string.h>
  11  #include <util/threadpool.h>
  12  
  13  #include <boost/test/unit_test.hpp>
  14  
  15  using http_bitcoin::GetQueryParameterFromUri;
  16  using http_bitcoin::HTTPHeaders;
  17  using http_bitcoin::HTTPRemoteClient;
  18  using http_bitcoin::HTTPRequest;
  19  using http_bitcoin::HTTPResponse;
  20  using http_bitcoin::HTTPServer;
  21  using http_bitcoin::MAX_BODY_SIZE;
  22  using http_bitcoin::MAX_HEADERS_SIZE;
  23  using util::LineReader;
  24  
  25  // HTTP request captured from bitcoin-cli
  26  constexpr std::string_view full_request = "POST / HTTP/1.1\r\n"
  27                                            "Host: 127.0.0.1\r\n"
  28                                            "Connection: close\r\n"
  29                                            "Content-Type: application/json\r\n"
  30                                            "Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
  31                                            "Content-Length: 46\r\n"
  32                                            "\r\n"
  33                                            R"({"method":"getblockcount","params":[],"id":1})""\n";
  34  
  35  BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
  36  
  37  BOOST_AUTO_TEST_CASE(test_query_parameters)
  38  {
  39      std::string uri {};
  40  
  41      // Tolerate a URI with invalid characters (% not followed by hex digits)
  42      uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
  43      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
  44      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");
  45  
  46      // No parameters
  47      uri = "localhost:8080/rest/headers/someresource.json";
  48      BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
  49  
  50      // Single parameter
  51      uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
  52      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
  53      BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));
  54  
  55      // Multiple parameters
  56      uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
  57      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
  58      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");
  59  
  60      // If the query string contains duplicate keys, the first value is returned
  61      uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
  62      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
  63  
  64      // Invalid query string syntax is the same as not having parameters
  65      uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
  66      BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
  67  
  68      // Multiple parameters, some characters encoded
  69      uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
  70      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
  71      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");
  72  
  73      // Encoded query delimiters are part of the parameter value, not structure.
  74      uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
  75      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
  76      BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");
  77  
  78      // An encoded question mark in the path does not introduce a query section.
  79      uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
  80      BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
  81  }
  82  
  83  BOOST_AUTO_TEST_CASE(http_headers_tests)
  84  {
  85      {
  86          // Writing response headers
  87          HTTPHeaders headers{};
  88          BOOST_CHECK(!headers.FindFirst("Cache-Control"));
  89          headers.Write("Cache-Control", "no-cache");
  90          // Check case-insensitive key matching
  91          BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
  92          BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
  93          // Additional values are appended, compared case-insensitive
  94          headers.Write("cache-control", "max-age=60");
  95          BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
  96          BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
  97          // Add a few more
  98          headers.Write("Pie", "apple");
  99          headers.Write("Sandwich", "ham");
 100          headers.Write("Coffee", "black");
 101          BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
 102          // Remove
 103          headers.RemoveAll("Pie");
 104          BOOST_CHECK(!headers.FindFirst("Pie"));
 105          // Combine for transmission
 106          std::string headers_string{headers.Stringify()};
 107          BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
 108                                            "cache-control: max-age=60\r\n"
 109                                            "Sandwich: ham\r\n"
 110                                            "Coffee: black\r\n"
 111                                            "\r\n");
 112      }
 113      {
 114          // Reading request headers captured from bitcoin-cli
 115          constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
 116                                                           "Connection: close\r\n"
 117                                                           "Content-Type: application/json\r\n"
 118                                                           "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
 119                                                           "Content-Length: 46\r\n";
 120          util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
 121          HTTPHeaders headers{};
 122          headers.Read(reader);
 123          BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
 124          BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
 125          BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
 126          BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
 127          BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
 128          BOOST_CHECK(!headers.FindFirst("Pizza"));
 129      }
 130      // Ensure invalid headers are rejected
 131      {
 132          // missing a colon
 133          util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
 134          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
 135      }
 136      {
 137          // missing a key
 138          util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
 139          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
 140      }
 141      {
 142          // contains NUL
 143          util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
 144          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
 145      }
 146      {
 147          // contains bare \r (not followed by \n)
 148          util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
 149          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
 150      }
 151      {
 152          // contains odd \r preceding the expected CRLF
 153          util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
 154          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
 155      }
 156      {
 157          // key contains whitespace
 158          util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
 159          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
 160      }
 161      {
 162          // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
 163          std::string lines;
 164          lines.reserve(820 * 10);
 165          for (int i = 0; i < 820; ++i) {
 166              lines.append("key:value\n");
 167          }
 168          std::string_view excessive_headers{lines};
 169          BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE);
 170          util::LineReader reader{excessive_headers, /*max_line_length=*/MAX_HEADERS_SIZE};
 171          BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
 172      }
 173      {
 174          // Ok
 175          util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
 176          HTTPHeaders headers{};
 177          headers.Read(reader);
 178          BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value");
 179      }
 180  }
 181  
 182  BOOST_AUTO_TEST_CASE(http_response_tests)
 183  {
 184      // Typical HTTP 1.1 response headers
 185      HTTPHeaders headers{};
 186      headers.Write("Content-Length", "41");
 187  
 188      // Response points to headers which already exist because some of them
 189      // are set before we even know what the response will be.
 190      HTTPResponse res;
 191      res.m_version = {.major = 1, .minor = 1};
 192      res.m_status = HTTP_OK;
 193      res.m_headers = std::move(headers);
 194      BOOST_CHECK_EQUAL(
 195          res.StringifyHeaders(),
 196          "HTTP/1.1 200 OK\r\n"
 197          "Content-Length: 41\r\n"
 198          "\r\n");
 199  }
 200  
 201  BOOST_AUTO_TEST_CASE(http_request_tests)
 202  {
 203      {
 204          HTTPRequest req;
 205          LineReader reader(full_request, MAX_HEADERS_SIZE);
 206          BOOST_CHECK(req.LoadControlData(reader));
 207          BOOST_CHECK(req.LoadHeaders(reader));
 208          BOOST_CHECK(req.LoadBody(reader));
 209          BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::POST);
 210          BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::POST);
 211          BOOST_CHECK_EQUAL(req.m_target, "/");
 212          BOOST_CHECK_EQUAL(req.GetURI(), "/");
 213          BOOST_CHECK_EQUAL(req.m_version.major, 1);
 214          BOOST_CHECK_EQUAL(req.m_version.minor, 1);
 215          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
 216          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
 217          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
 218          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
 219          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
 220          BOOST_CHECK_EQUAL(req.m_body.size(), 46);
 221          BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
 222      }
 223      {
 224          // Malformed: no spaces between data
 225          HTTPRequest req;
 226          LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 227          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
 228      }
 229      {
 230          // Malformed: too many spaces
 231          HTTPRequest req;
 232          LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 233          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
 234      }
 235      {
 236          // Malformed: slash missing before version
 237          HTTPRequest req;
 238          LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 239          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
 240      }
 241      {
 242          // Malformed: no decimal in version
 243          HTTPRequest req;
 244          LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 245          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
 246      }
 247      {
 248          // Malformed: version is not a number
 249          HTTPRequest req;
 250          LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 251          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
 252      }
 253      {
 254          // Malformed: version is out of range
 255          HTTPRequest req;
 256          LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 257          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
 258      }
 259      {
 260          // Malformed: version is out of range
 261          HTTPRequest req;
 262          LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 263          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
 264      }
 265      {
 266          // Malformed: version is out of range
 267          HTTPRequest req;
 268          LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 269          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
 270      }
 271      {
 272          // Malformed: version is not exactly two integers and a dot
 273          HTTPRequest req;
 274          LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 275          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
 276      }
 277      {
 278          // Malformed: contains NUL
 279          HTTPRequest req;
 280          LineReader reader{std::string_view{"GET /safe\0/etc/passwd HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", 50}, MAX_HEADERS_SIZE};
 281          BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
 282      }
 283      {
 284          // Malformed: differing Content-Length values, case insensitive
 285          constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
 286                                                        "Host: 127.0.0.1\n"
 287                                                        "Content-Length: 8\n"
 288                                                        "content-length: 9\n\n"
 289                                                        "12345678";
 290          HTTPRequest req;
 291          util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
 292          BOOST_CHECK(req.LoadControlData(reader));
 293          BOOST_CHECK(req.LoadHeaders(reader));
 294          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
 295      }
 296      {
 297          // Ok: multiple same Content-Length values
 298          constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
 299                                                        "Host: 127.0.0.1\n"
 300                                                        "Content-Length: 8\n"
 301                                                        "content-length: 8\n\n"
 302                                                        "12345678";
 303          HTTPRequest req;
 304          util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
 305          BOOST_CHECK(req.LoadControlData(reader));
 306          BOOST_CHECK(req.LoadHeaders(reader));
 307          BOOST_CHECK(req.LoadBody(reader));
 308      }
 309      {
 310          // Ok
 311          HTTPRequest req;
 312          LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 313          BOOST_CHECK(req.LoadControlData(reader));
 314          BOOST_CHECK(req.LoadHeaders(reader));
 315          BOOST_CHECK(req.LoadBody(reader));
 316          BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::GET);
 317          BOOST_CHECK_EQUAL(req.m_target, "/");
 318          BOOST_CHECK_EQUAL(req.m_version.major, 1);
 319          BOOST_CHECK_EQUAL(req.m_version.minor, 0);
 320          BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
 321          // no body is OK
 322          BOOST_CHECK_EQUAL(req.m_body.size(), 0);
 323      }
 324      {
 325          // Malformed: missing colon
 326          HTTPRequest req;
 327          LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
 328          BOOST_CHECK(req.LoadControlData(reader));
 329          BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
 330      }
 331      {
 332          // We might not have received enough data from the client which is not
 333          // an error. We return false so the caller can try again later when the
 334          // buffer has more data.
 335          HTTPRequest req;
 336          LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
 337          BOOST_CHECK(req.LoadControlData(reader));
 338          BOOST_CHECK(!req.LoadHeaders(reader));
 339      }
 340      {
 341          // No Content-Length: body is not read
 342          HTTPRequest req;
 343          LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
 344          BOOST_CHECK(req.LoadControlData(reader));
 345          BOOST_CHECK(req.LoadHeaders(reader));
 346          BOOST_CHECK(req.LoadBody(reader));
 347          // Don't try to read request body if Content-Length is missing
 348          BOOST_CHECK_EQUAL(req.m_body.size(), 0);
 349      }
 350      {
 351          // Malformed: Content-Length is not a number
 352          HTTPRequest req;
 353          LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
 354          BOOST_CHECK(req.LoadControlData(reader));
 355          BOOST_CHECK(req.LoadHeaders(reader));
 356          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
 357      }
 358      {
 359          // Malformed: Content-Length is negative
 360          HTTPRequest req;
 361          LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
 362          BOOST_CHECK(req.LoadControlData(reader));
 363          BOOST_CHECK(req.LoadHeaders(reader));
 364          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
 365      }
 366      {
 367          // Content-Length exceeds limit
 368          constexpr auto excessive_size{MAX_BODY_SIZE + 1};
 369          std::string huge_body(excessive_size, 'x');
 370          const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
 371          HTTPRequest req;
 372          LineReader reader(request, MAX_HEADERS_SIZE);
 373          BOOST_CHECK(req.LoadControlData(reader));
 374          BOOST_CHECK(req.LoadHeaders(reader));
 375          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Max body size exceeded"});
 376      }
 377      {
 378          // Content-Length exactly on the limit
 379          std::string max_body(MAX_BODY_SIZE, 'x');
 380          const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
 381          HTTPRequest req;
 382          LineReader reader(request, MAX_HEADERS_SIZE);
 383          BOOST_CHECK(req.LoadControlData(reader));
 384          BOOST_CHECK(req.LoadHeaders(reader));
 385          BOOST_CHECK(req.LoadBody(reader));
 386      }
 387      {
 388          // Content-Length indicates more data than we have in the buffer.
 389          // Not an error; we wait for more data before completing the body.
 390          HTTPRequest req;
 391          LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
 392          BOOST_CHECK(req.LoadControlData(reader));
 393          BOOST_CHECK(req.LoadHeaders(reader));
 394          BOOST_CHECK(!req.LoadBody(reader));
 395      }
 396      {
 397          // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
 398          HTTPRequest req;
 399          std::string_view ok_chunked = "GET / HTTP/1.0\n"
 400                                        "Transfer-Encoding: chunked\n"
 401                                        "\n"
 402                                        "10\n"
 403                                        R"({"method":"getbl)""\n"
 404                                        "   a    \n"
 405                                        R"(ockcount"})""\n"
 406                                        "0\n"
 407                                        "\n";
 408          LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
 409          BOOST_CHECK(req.LoadControlData(reader));
 410          BOOST_CHECK(req.LoadHeaders(reader));
 411          BOOST_CHECK(req.LoadBody(reader));
 412          BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
 413      }
 414      {
 415          // Prevent "chunked" transfer from exceeding size limit
 416          HTTPRequest req;
 417          std::string_view excessive_chunk_size = "GET / HTTP/1.0\n"
 418                                                  "Transfer-Encoding: chunked\n"
 419                                                  "\n"
 420                                                  "10\n"
 421                                                  R"({"method":"getbl)""\n"
 422                                                  "20000000\n"
 423                                                  R"(ockcount"})""\n"
 424                                                  "0\n"
 425                                                  "\n";
 426          LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
 427          BOOST_CHECK(req.LoadControlData(reader));
 428          BOOST_CHECK(req.LoadHeaders(reader));
 429          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
 430      }
 431      {
 432          // Allow (but ignore) Chunk Extensions
 433          HTTPRequest req;
 434          std::string_view ok_chunked = "GET / HTTP/1.0\n"
 435                                        "Transfer-Encoding: chunked\n"
 436                                        "\n"
 437                                        "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
 438                                        R"({"method":"getbl)""\n"
 439                                        "   a   ;    compressed\n"
 440                                        R"(ockcount"})""\n"
 441                                        "0;why;would;anyone;do;this;\n"
 442                                        "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
 443                                        "\n";
 444          LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
 445          BOOST_CHECK(req.LoadControlData(reader));
 446          BOOST_CHECK(req.LoadHeaders(reader));
 447          BOOST_CHECK(req.LoadBody(reader));
 448          BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
 449          // Chunk Trailer was cleared
 450          BOOST_CHECK_EQUAL(reader.Remaining(), 0);
 451      }
 452      {
 453          // Invalid "chunked" transfer, using roman numerals instead of hex for chunk length
 454          HTTPRequest req;
 455          std::string_view invalid_chunked = "GET / HTTP/1.0\n"
 456                                             "Transfer-Encoding: chunked\n"
 457                                             "\n"
 458                                             "XVI\n"
 459                                             R"({"method":"getbl)""\n"
 460                                             "X\n"
 461                                             R"(ockcount"})""\n"
 462                                             "0\n"
 463                                             "\n";
 464          LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
 465          BOOST_CHECK(req.LoadControlData(reader));
 466          BOOST_CHECK(req.LoadHeaders(reader));
 467          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
 468      }
 469      {
 470          // Invalid "chunked" transfer, missing chunk termination \n
 471          HTTPRequest req;
 472          std::string_view invalid_chunked = "GET / HTTP/1.0\n"
 473                                             "Transfer-Encoding: chunked\n"
 474                                             "\n"
 475                                             "10\n"
 476                                             R"({"method":"getbl)"
 477                                             "a\n" // interpreted as extra data at the end of `0x10`-sized chunk
 478                                             R"(ockcount"})"
 479                                             "0\n"
 480                                             "\n";
 481          LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
 482          BOOST_CHECK(req.LoadControlData(reader));
 483          BOOST_CHECK(req.LoadHeaders(reader));
 484          BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
 485      }
 486      {
 487          // End of buffer reached without chunk termination, caller must wait for more data to arrive
 488          HTTPRequest req;
 489          std::string delayed_chunked = "GET / HTTP/1.0\n"
 490                                        "Transfer-Encoding: chunked\n"
 491                                        "\n"
 492                                        "10\n"
 493                                        R"({"method":"getbl)""\n"
 494                                        "a\n"
 495                                        R"(ockcount"})";
 496          LineReader reader1(delayed_chunked, MAX_HEADERS_SIZE);
 497          BOOST_CHECK(req.LoadControlData(reader1));
 498          BOOST_CHECK(req.LoadHeaders(reader1));
 499          BOOST_CHECK(!req.LoadBody(reader1));
 500          // more data arrives!
 501          delayed_chunked += "\n0\n\n";
 502          LineReader reader2(delayed_chunked, MAX_HEADERS_SIZE);
 503          BOOST_CHECK(req.LoadControlData(reader2));
 504          BOOST_CHECK(req.LoadHeaders(reader2));
 505          BOOST_CHECK(req.LoadBody(reader2));
 506      }
 507  }
 508  
 509  BOOST_AUTO_TEST_CASE(http_server_socket_tests)
 510  {
 511      // Hard code the timestamp for the Date header in the HTTP response
 512      // Wed Dec 11 00:47:09 2024 UTC
 513      SetMockTime(1733878029);
 514  
 515      // Prepare a request handler that just stores received requests so we can examine them.
 516      // Mutex is required to prevent a race between this test's main thread and the server's I/O loop.
 517      Mutex requests_mutex;
 518      std::deque<std::unique_ptr<HTTPRequest>> requests;
 519      auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
 520          LOCK(requests_mutex);
 521          requests.push_back(std::move(req));
 522      };
 523  
 524      HTTPServer server{StoreRequest};
 525  
 526      {
 527          // We can only bind to NET_IPV4 and NET_IPV6
 528          CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
 529          auto result{server.BindAndStartListening(onion_address)};
 530          BOOST_REQUIRE(!result);
 531          BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
 532      }
 533  
 534      // This VALID address won't actually get used because we stubbed CreateSock()
 535      CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
 536  
 537      // Init state
 538      BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
 539      // Bind to mock Listening Socket
 540      BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
 541      // We are bound and listening
 542      BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
 543  
 544      // Start the I/O loop
 545      server.StartSocketsThreads();
 546  
 547      // No connections yet
 548      BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);
 549  
 550      // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.
 551      // Keep a handle for the mock client's send and receive pipes so we can examine
 552      // the data it "receives".
 553      std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};
 554  
 555      // Wait up to a minute to find and connect the client in the I/O loop
 556      int attempts{6000};
 557      while (server.GetConnectionsCount() < 1) {
 558          std::this_thread::sleep_for(10ms);
 559          BOOST_REQUIRE(--attempts > 0);
 560      }
 561  
 562      // Prepare a pointer to the client, we'll assign it from the request itself.
 563      std::shared_ptr<HTTPRemoteClient> client;
 564  
 565      // Wait up to a minute to read the request from the client.
 566      // Given that the mock client is itself a mock socket
 567      // with hard-coded data it should only take a fraction of that.
 568      attempts = 6000;
 569      while (true) {
 570          {
 571              LOCK(requests_mutex);
 572              // Connected client should have one request already from the static content.
 573              if (requests.size() == 1) {
 574                  // Check the received request
 575                  BOOST_CHECK_EQUAL(requests.front()->m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
 576                  BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");
 577  
 578                  // Inspect the connection pointed to from the request
 579                  client = requests.front()->m_client;
 580                  BOOST_CHECK_EQUAL(client->m_origin, "5.5.5.5:6789");
 581  
 582                  // Respond to request
 583                  requests.front()->WriteReply(HTTP_OK, "874140\n");
 584  
 585                  break;
 586              }
 587          }
 588          std::this_thread::sleep_for(10ms);
 589          BOOST_REQUIRE(--attempts > 0);
 590      }
 591  
 592      // Check the sent response from the mock client at the other end of the mock socket
 593      std::string actual;
 594      // Wait up to one minute for all the bytes to appear in the "send" pipe.
 595      char buf[0x10000] = {};
 596      attempts = 6000;
 597      while (attempts > 0)
 598      {
 599          ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
 600          if (bytes_read > 0) {
 601              actual.append(buf, bytes_read);
 602              if (actual.length() == 146) {
 603                  break;
 604              }
 605          }
 606          std::this_thread::sleep_for(10ms);
 607          --attempts;
 608      }
 609      BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
 610      BOOST_CHECK(actual.ends_with("\r\n874140\n"));
 611      // Headers can be sorted in any order, and will be, since we use unordered_map
 612      BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
 613      BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
 614      BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
 615      BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);
 616  
 617      // Wait up to one minute for connection to be automatically closed, because
 618      // keep-alive was not set by the client and we are done responding to their request.
 619      attempts = 6000;
 620      while (server.GetConnectionsCount() != 0) {
 621          std::this_thread::sleep_for(10ms);
 622          BOOST_REQUIRE(--attempts > 0);
 623      }
 624  
 625      // Stop the I/O loop and shutdown
 626      server.InterruptNet();
 627      // Wait for I/O loop to finish, after all connected sockets are closed
 628      server.JoinSocketsThreads();
 629      // Close all listening sockets
 630      server.StopListening();
 631  }
 632  
 633  BOOST_AUTO_TEST_CASE(http_socket_error_tests)
 634  {
 635      // Create a tiny threadpool for the HTTPRequest handler
 636      ThreadPool workers("http");
 637      workers.Start(1);
 638  
 639      // Hard-code the server's request handler to respond to each request with
 640      // an incremented block count. Handle the replies in the worker thread.
 641      std::atomic<int> height{0};
 642      HTTPServer server{[&](std::shared_ptr<HTTPRequest> req) {
 643          auto item = [req, &height]() {
 644              const int h = height.fetch_add(1);
 645              req->WriteReply(HTTP_OK, strprintf("height: %d\n", h));
 646          };
 647          // Can't call BOOST_REQUIRE from worker thread
 648          Assert(workers.Submit(std::move(item)));
 649      }};
 650  
 651      // All replies will be the same size
 652      static constexpr std::size_t reply_length = std::string_view{
 653          "HTTP/1.1 200 OK\r\n"
 654          "Date: Thu, 01 Jan 2026 00:00:00 GMT\r\n"           // All RFC1123 dates are 29 characters
 655          "Content-Length: 10\r\n"
 656          "Content-Type: text/html; charset=ISO-8859-1\r\n"
 657          "\r\n"
 658          "height: 0\n"
 659      }.size();
 660  
 661      /**
 662       * A mocked Sock derived from DynSock whose Send() only succeeds when there is more than
 663       * one reply being sent (send buffer length > reply_length). Otherwise it returns
 664       * a recoverable error (WSAEAGAIN).
 665       *
 666       * After it sends successfully once, it continues to always succeed.
 667       *
 668       * Useful for testing "try again" logic around non-blocking socket Send() failures.
 669       */
 670      class ErrorSock : public DynSock
 671      {
 672      public:
 673          explicit ErrorSock(std::shared_ptr<Pipes> pipes) : DynSock{std::move(pipes)} {}
 674          DynSock& operator=(Sock&&) override { assert(false); return *this; }
 675  
 676          ssize_t Send(const void* buf, size_t len, int flags) const override
 677          {
 678              if (len <= reply_length && !m_have_sent) {
 679                  #ifdef WIN32
 680                  WSASetLastError(WSAEWOULDBLOCK);
 681                  #else
 682                  errno = WSAEAGAIN;
 683                  #endif
 684                  return -1;
 685              } else {
 686                  m_have_sent = true;
 687                  return DynSock::Send(buf, len, flags);
 688              }
 689          }
 690  
 691          mutable bool m_have_sent{false};
 692      };
 693  
 694      // Simpler server startup than the last test
 695      CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
 696      BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
 697      server.StartSocketsThreads();
 698  
 699      // Prepare initial requests
 700      int num_requests = 2;
 701      // Use keep-alive so the server holds the connection open for all requests.
 702      std::string keepalive_request{full_request};
 703      keepalive_request.replace(keepalive_request.find("Connection: close"), 17, "Connection: keep-alive");
 704      // Combine all requests so they are read from the socket on a single iteration of the I/O loop
 705      std::string all_requests;
 706      for (int i = 0; i < num_requests; i++) {
 707          all_requests += keepalive_request;
 708      }
 709  
 710      // Watch the log messages to ensure that the first two replies were sent
 711      // together. This indicates the non-optimistic send path was used
 712      // because a reply was already sitting in the send buffer when a second reply
 713      // was added.
 714      DebugLogHelper find_two_replies{strprintf("Sent %d bytes to client", reply_length * 2),
 715                                      [&](const std::string* s) {
 716                                          return true;
 717                                      }};
 718      // Last reply should be sent on its own by optimistic send path, because
 719      // the send buffer was empty when the reply was written.
 720      DebugLogHelper find_one_reply{strprintf("Sent %d bytes to client", reply_length),
 721                                    [&](const std::string* s) {
 722                                        return true;
 723                                     }};
 724  
 725      // Connect the ErrorSock as mock client with the preloaded data and get a handle on the I/O pipes
 726      std::shared_ptr<ErrorSock::Pipes> mock_client_socket_pipes{
 727          ConnectClient<ErrorSock>(std::as_bytes(std::span(all_requests)))
 728      };
 729  
 730      // Wait up to one minute for the last reply from the server
 731      std::string actual;
 732      char buf[0x10000] = {};
 733      int attempts = 6000;
 734      while (attempts > 0)
 735      {
 736          ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
 737          if (bytes_read > 0) {
 738              actual.append(buf, bytes_read);
 739              if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
 740                  break;
 741              }
 742          }
 743          std::this_thread::sleep_for(10ms);
 744          --attempts;
 745      }
 746  
 747      // Send the third request.
 748      // If there was a race between WriteReply() in the worker thread setting m_send_ready=true
 749      // and SocketHandlerConnected() in the I/O thread flushing the send buffer,
 750      // then the socket would be stuck in write mode with nothing to write,
 751      // the server would never read from the socket, and this request would time out.
 752      // Wait a second to ensure both the worker thread and I/O thread are idle.
 753      // If we send the next request too soon it might get accepted by the server before
 754      // it gets wedged shut.
 755      std::this_thread::sleep_for(1000ms);
 756      mock_client_socket_pipes->recv.PushBytes(keepalive_request.data(), keepalive_request.size());
 757      num_requests++;
 758  
 759      // Wait up to one minute for reply
 760      attempts = 6000;
 761      while (attempts > 0)
 762      {
 763          ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
 764          if (bytes_read > 0) {
 765              actual.append(buf, bytes_read);
 766              if (actual.find(strprintf("height: %d", num_requests - 1)) != std::string::npos) {
 767                  break;
 768              }
 769          }
 770          std::this_thread::sleep_for(10ms);
 771          --attempts;
 772      }
 773  
 774      // All replies were received
 775      for (int i = 0; i < num_requests; i++) {
 776          BOOST_REQUIRE(actual.find(strprintf("height: %d", i)) != std::string::npos);
 777      }
 778  
 779      // Close the keep-alive connection
 780      server.DisconnectAllClients();
 781  
 782      workers.Stop();
 783  
 784      server.InterruptNet();
 785      server.JoinSocketsThreads();
 786      server.StopListening();
 787  }
 788  
 789  BOOST_AUTO_TEST_SUITE_END()
 790