Commit 02aa0c1f authored by jan.koester's avatar jan.koester
Browse files

tests

parent 2d61263d
Loading
Loading
Loading
Loading
+36 −0
Changes for test/CMakeLists.txt: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -45,3 +45,39 @@ if(WIN32)
else()
    target_link_libraries (client_test httppp-static)
endif()

add_executable(http1_checksum_test
    http1_checksum_test.cpp
)

if(WIN32)
    target_link_libraries (http1_checksum_test httppp-static ws2_32)
else()
    target_link_libraries (http1_checksum_test httppp-static)
endif()

add_test(http1_checksum_test http1_checksum_test)

add_executable(http2_checksum_test
    http2_checksum_test.cpp
)

if(WIN32)
    target_link_libraries (http2_checksum_test httppp-static ws2_32)
else()
    target_link_libraries (http2_checksum_test httppp-static)
endif()

add_test(http2_checksum_test http2_checksum_test)

add_executable(http3_checksum_test
    http3_checksum_test.cpp
)

if(WIN32)
    target_link_libraries (http3_checksum_test httppp-static ws2_32)
else()
    target_link_libraries (http3_checksum_test httppp-static)
endif()

add_test(http3_checksum_test http3_checksum_test)

test/checksum_common.h

0 → 100644
+161 −0
Changes for test/checksum_common.h: 161 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// Shared checksum/random-payload helpers and request handler for the
// HTTP/1.1, HTTP/2 and HTTP/3 checksum integrity tests
// (http1_checksum_test.cpp, http2_checksum_test.cpp, http3_checksum_test.cpp).
//
// The three protocol tests each drive a real client<->server exchange over
// their own wire format, but they all exercise the SAME server-side
// RequestEvent, because HttpEvent::RequestEvent() gets the request body in
// curreq.RecvData and lets a response header be attached via
// setHeaderData() identically regardless of protocol -- HTTP/2 and HTTP/3
// forward extra HttpResponse headers to the wire via the library's internal
// ":res-*" pseudo-header convention (see HttpResponse::_storeResponseInfo /
// httpd.cpp's _finishH2Dispatch), so a single handler genuinely covers all
// three protocols.

#pragma once

#include <cstdint>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

#include <netplus/crypto/sha.h>
#include <netplus/random.h>

#include "http.h"
#include "httpd.h"

namespace checksumtest {

inline std::string sha256Hex(const char *data, size_t len) {
    std::vector<uint8_t> buf(reinterpret_cast<const uint8_t *>(data),
                              reinterpret_cast<const uint8_t *>(data) + len);
    std::vector<uint8_t> digest = netplus::sha256_hash(buf);
    static const char *hexd = "0123456789abcdef";
    std::string out;
    out.reserve(digest.size() * 2);
    for (uint8_t b : digest) {
        out.push_back(hexd[b >> 4]);
        out.push_back(hexd[b & 0x0f]);
    }
    return out;
}

inline std::string sha256Hex(const std::vector<char> &data) {
    return sha256Hex(data.data(), data.size());
}

inline std::vector<char> randomBuffer(size_t len) {
    std::vector<char> buf(len);
    if (len)
        netplus::fillRandomBytes(reinterpret_cast<uint8_t *>(buf.data()), len);
    return buf;
}

// Small-to-large sweep: an empty body, a single byte, a few KB, one that
// spans several TCP/TLS records or H2/H3 frames (64KB) and one that is
// guaranteed to exhaust the HTTP/2 64KB-ish default flow-control window and
// force multiple round trips (1MB).
inline const std::vector<size_t> &sweepSizes() {
    static const std::vector<size_t> sizes = {0, 1, 4096, 65536, 1048576};
    return sizes;
}

// GET  /gen/<n>  -> <n> random bytes, with X-Sha256 holding their checksum.
// POST /echo     -> echoes the request body back, with X-Sha256 holding the
//                   checksum of what the server actually received (so
//                   upload integrity is verified independently of what the
//                   client remembers sending, and download integrity is
//                   verified by the client comparing the echoed bytes to
//                   its own copy).
class ChecksumEvent : public libhttppp::HttpEvent {
public:
    explicit ChecksumEvent(std::vector<netplus::socket *> serversocket)
        : libhttppp::HttpEvent(serversocket) {}

    void RequestEvent(libhttppp::HttpRequest &req, const int, ULONG_PTR) override {
        try {
            const std::string &url = req.getRequestURL();

            if (url.rfind("/gen/", 0) == 0) {
                size_t len = 0;
                try { len = static_cast<size_t>(std::stoull(url.substr(5))); }
                catch (...) { len = 0; }

                std::vector<char> body = randomBuffer(len);
                std::string hash = sha256Hex(body);

                libhttppp::HttpResponse res;
                res.setContentType("application/octet-stream");
                res.setHeaderData("x-sha256")->push_back(hash);
                res.send(req, body, static_cast<int>(body.size()));
                return;
            }

            if (url == "/echo") {
                std::vector<char> body(req.RecvData.data(),
                                        req.RecvData.data() + req.RecvData.size());
                std::string hash = sha256Hex(body);

                libhttppp::HttpResponse res;
                res.setContentType("application/octet-stream");
                res.setHeaderData("x-sha256")->push_back(hash);
                res.send(req, body, static_cast<int>(body.size()));
                return;
            }

            libhttppp::HttpResponse res;
            res.setState("404 Not Found");
            res.send(req, std::string("not found"), 9);
        } catch (libhttppp::HTTPException &e) {
            std::cerr << "[ChecksumEvent] " << e.what() << std::endl;
        }
    }
};

// RAII stop/join for a server thread running HttpEvent::runEventloop().
// Letting such a thread outlive its netplus::event/socket objects (e.g. via
// detach()) races epoll teardown (netplus::poll's CloseEventHandler)
// against object/process teardown and can segfault; joining it -- on every
// exit path, including exceptions -- avoids that race. Declare this AFTER
// the socket/event objects it targets so it destructs (and joins) BEFORE
// them, in a scope that has already started the thread.
struct ServerThreadGuard {
    std::thread *t;
    ~ServerThreadGuard() {
        if (t && t->joinable()) {
            netplus::event::Running = false;
            t->join();
        }
    }
};

} // namespace checksumtest
+339 −0
Changes for test/http1_checksum_test.cpp: 339 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// HTTP/1.1 checksum integrity test: drives a real HttpClient against a real
// HttpEvent-based server over plain TCP, sweeping payload sizes from empty
// to 1MB for both GET (server-generated, checksummed via X-Sha256) and POST
// (echoed back, checksummed both ways), plus:
//   - a raw hand-built "Transfer-Encoding: chunked" POST to exercise
//     HttpRequest::isChunkedRequest()/decodeChunkedBody() on the server, and
//   - a chunked GET response (HttpResponse::sendChunkedHeaders/sendChunk/
//     endChunked, irregular chunk sizes) read back transparently by
//     HttpClient's chunked-response reader, and
//   - HttpClient::GetStream()/readBodyChunk() incremental streaming reads
//     hashed as they arrive, matching the server's checksum.

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <thread>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <netplus/exception.h>
#include <netplus/socket.h>

#include "checksum_common.h"

using namespace checksumtest;

namespace {

int g_passed = 0, g_failed = 0;

void check(bool ok, const std::string &name) {
    if (ok) { std::cout << "  PASS: " << name << std::endl; ++g_passed; }
    else    { std::cout << "  FAIL: " << name << std::endl; ++g_failed; }
}

// Adds one H1-only path ("/chunked/<n>") that streams its response out via
// real chunked transfer-encoding with irregular chunk sizes, on top of the
// protocol-agnostic ChecksumEvent handler shared with the H2/H3 tests.
class H1ChecksumEvent : public ChecksumEvent {
public:
    using ChecksumEvent::ChecksumEvent;

    void RequestEvent(libhttppp::HttpRequest &req, const int tid, ULONG_PTR args) override {
        const std::string &url = req.getRequestURL();
        if (url.rfind("/chunked/", 0) == 0) {
            size_t len = 0;
            try { len = static_cast<size_t>(std::stoull(url.substr(9))); }
            catch (...) { len = 0; }

            std::vector<char> body = randomBuffer(len);
            std::string hash = sha256Hex(body);

            libhttppp::HttpResponse res;
            res.setContentType("application/octet-stream");
            res.setHeaderData("x-sha256")->push_back(hash);
            res.sendChunkedHeaders(req);

            static const size_t kChunkSizes[] = {1, 17, 4096, 65536};
            size_t off = 0, ci = 0;
            while (off < body.size()) {
                size_t take = std::min(kChunkSizes[ci % 4], body.size() - off);
                res.sendChunk(req, body.data() + off, take);
                off += take;
                ++ci;
            }
            res.endChunked(req);
            return;
        }
        ChecksumEvent::RequestEvent(req, tid, args);
    }
};

int startServer(std::unique_ptr<netplus::tcp> &sockOwner,
                 std::unique_ptr<H1ChecksumEvent> &serverOwner,
                 std::thread &serverThread) {
    sockOwner = std::make_unique<netplus::tcp>("127.0.0.1", 0, 16, -1);
    sockOwner->bind();

    sockaddr_in bound{};
    socklen_t blen = sizeof(bound);
    ::getsockname(sockOwner->fd(), reinterpret_cast<sockaddr *>(&bound), &blen);
    int port = ntohs(bound.sin_port);

    sockOwner->listen();

    serverOwner = std::make_unique<H1ChecksumEvent>(
        std::vector<netplus::socket *>{sockOwner.get()});
    serverThread = std::thread([s = serverOwner.get()] { s->runEventloop(); });
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    return port;
}

std::string headerValue(const libhttppp::HttpResponse &res, const std::string &key) {
    auto *hd = res.getHeaderData(key);
    if (hd && hd->getfirstValue())
        return hd->getfirstValue()->getvalue();
    return {};
}

// ---- Raw-socket helpers for cases HttpClient doesn't cover directly ----

int rawConnect(int port) {
    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(static_cast<uint16_t>(port));
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (::connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
        ::close(fd);
        throw std::runtime_error("raw connect failed");
    }
    return fd;
}

void sendAll(int fd, const std::string &data) {
    size_t sent = 0;
    while (sent < data.size()) {
        ssize_t n = ::send(fd, data.data() + sent, data.size() - sent, 0);
        if (n <= 0) throw std::runtime_error("raw send failed");
        sent += static_cast<size_t>(n);
    }
}

// Reads one non-chunked HTTP/1.1 response (headers + Content-Length body)
// off a raw fd, reusing the library's own HttpResponse::parse() so the
// header-parsing logic under test is the real thing, not a re-implementation.
std::vector<char> rawReadResponse(int fd, libhttppp::HttpResponse &res) {
    std::vector<char> raw;
    char tmp[8192];
    size_t header_end = std::string::npos;

    for (;;) {
        if (raw.size() >= 4) {
            for (size_t i = 0; i + 3 < raw.size(); ++i) {
                if (raw[i] == '\r' && raw[i + 1] == '\n' &&
                    raw[i + 2] == '\r' && raw[i + 3] == '\n') {
                    header_end = i + 4;
                    break;
                }
            }
        }
        if (header_end != std::string::npos) break;
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) throw std::runtime_error("raw recv: EOF before headers complete");
        raw.insert(raw.end(), tmp, tmp + n);
    }

    size_t parsed = res.parse(raw.data(), raw.size());
    size_t contentLength = res.getContentLength();

    std::vector<char> body(raw.begin() + static_cast<ptrdiff_t>(parsed), raw.end());
    while (body.size() < contentLength) {
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) throw std::runtime_error("raw recv: EOF before body complete");
        body.insert(body.end(), tmp, tmp + n);
    }
    body.resize(contentLength);
    return body;
}

void testRawChunkedUpload(int port) {
    std::vector<char> body = randomBuffer(150000);
    std::string expectedHash = sha256Hex(body);

    std::ostringstream req;
    req << "POST /echo HTTP/1.1\r\n"
        << "Host: 127.0.0.1:" << port << "\r\n"
        << "Transfer-Encoding: chunked\r\n"
        << "Connection: keep-alive\r\n"
        << "\r\n";

    // Deliberately irregular chunk sizes to stress decodeChunkedBody().
    static const size_t kChunkSizes[] = {3, 1, 4096, 65536};
    size_t off = 0, ci = 0;
    while (off < body.size()) {
        size_t take = std::min(kChunkSizes[ci % 4], body.size() - off);
        req << std::hex << take << std::dec << "\r\n";
        req.write(body.data() + off, static_cast<std::streamsize>(take));
        req << "\r\n";
        off += take;
        ++ci;
    }
    req << "0\r\n\r\n";

    int fd = rawConnect(port);
    sendAll(fd, req.str());

    libhttppp::HttpResponse res;
    std::vector<char> respBody = rawReadResponse(fd, res);
    ::close(fd);

    check(res.getStatusCode() == 200, "chunked upload: 200 response");
    check(headerValue(res, "x-sha256") == expectedHash,
          "chunked upload: server-reported sha256 matches client's");
    check(respBody.size() == body.size(),
          "chunked upload: echoed body size matches");
    check(respBody == body, "chunked upload: echoed bytes match exactly");
}

} // namespace

int main() {
    std::cout << "=== HTTP/1.1 Checksum Integrity Test ===" << std::endl;
    int rc = 0;

    try {
        std::unique_ptr<netplus::tcp> sock;
        std::unique_ptr<H1ChecksumEvent> server;
        std::thread serverThread;
        int port = startServer(sock, server, serverThread);
        ServerThreadGuard shutdownGuard{&serverThread};
        std::cout << "Server listening on 127.0.0.1:" << port << std::endl;

        libhttppp::HttpUrl url("http://127.0.0.1:" + std::to_string(port) + "/");
        libhttppp::HttpClient clt(url, /*vers=*/0, /*timeoutSec=*/5);

        // ---- GET/POST size sweep ----
        for (size_t size : sweepSizes()) {
            std::string label = "size=" + std::to_string(size);

            {
                libhttppp::HttpRequest req;
                req.setRequestURL("/gen/" + std::to_string(size));
                std::vector<char> body = clt.Get(req, /*maxTries=*/3);
                std::string want = headerValue(*clt.lastResponse(), "x-sha256");
                std::string got = sha256Hex(body);
                check(clt.lastStatusCode() == 200, "GET " + label + ": 200 response");
                check(body.size() == size, "GET " + label + ": body size matches");
                check(got == want, "GET " + label + ": sha256 matches X-Sha256 header");
            }

            {
                std::vector<char> payload = randomBuffer(size);
                std::string expectedHash = sha256Hex(payload);

                libhttppp::HttpRequest req;
                req.setRequestURL("/echo");
                std::vector<char> body = clt.Post(req, payload, /*maxTries=*/3);
                std::string reportedHash = headerValue(*clt.lastResponse(), "x-sha256");

                check(clt.lastStatusCode() == 200, "POST " + label + ": 200 response");
                check(body == payload, "POST " + label + ": echoed bytes match exactly");
                check(reportedHash == expectedHash,
                      "POST " + label + ": server-reported sha256 matches client's");
            }
        }

        // ---- Chunked transfer-encoding, both directions ----
        testRawChunkedUpload(port);

        {
            const size_t size = 262144;
            libhttppp::HttpRequest req;
            req.setRequestURL("/chunked/" + std::to_string(size));
            std::vector<char> body = clt.Get(req, /*maxTries=*/3);
            std::string want = headerValue(*clt.lastResponse(), "x-sha256");
            std::string got = sha256Hex(body);
            check(clt.lastStatusCode() == 200, "chunked download: 200 response");
            check(body.size() == size, "chunked download: body size matches");
            check(got == want, "chunked download: sha256 matches X-Sha256 header");
        }

        // ---- Streaming download via GetStream()/readBodyChunk() ----
        {
            const size_t size = 1048576;
            libhttppp::HttpRequest req;
            req.setRequestURL("/gen/" + std::to_string(size));
            libhttppp::HttpResponse res = clt.GetStream(req);
            std::string want = headerValue(res, "x-sha256");

            std::vector<char> collected;
            collected.reserve(size);
            char buf[4096];
            for (;;) {
                size_t n = clt.readBodyChunk(buf, sizeof(buf));
                if (n == 0) break;
                collected.insert(collected.end(), buf, buf + n);
            }
            std::string got = sha256Hex(collected);
            check(res.getStatusCode() == 200, "streaming GET: 200 response");
            check(collected.size() == size, "streaming GET: total bytes match");
            check(got == want, "streaming GET: incrementally-hashed sha256 matches header");
        }

        // shutdownGuard stops and joins the server thread here (its
        // destructor runs before sock/server as it's declared after them).

    } catch (libhttppp::HTTPException &e) {
        std::cerr << "HTTPException: " << e.what() << std::endl;
        rc = 1;
    } catch (netplus::NetException &e) {
        std::cerr << "NetException: " << e.what() << std::endl;
        rc = 1;
    } catch (std::exception &e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        rc = 1;
    }

    std::cout << "==============================" << std::endl;
    std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl;
    std::cout << "==============================" << std::endl;

    return (rc != 0 || g_failed > 0) ? 1 : 0;
}
+503 −0

File added.

Preview size limit exceeded, changes collapsed.

+204 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading