Commit a767bf97 authored by jan.koester's avatar jan.koester
Browse files

test

parent 084e0f90
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -153,3 +153,15 @@ else()
endif()

add_test(http_useragent_header_test http_useragent_header_test)

add_executable(httpd_h2_peer_address_test
    httpd_h2_peer_address_test.cpp
)

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

add_test(httpd_h2_peer_address_test httpd_h2_peer_address_test)
+230 −0
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.
*******************************************************************************/

// Regression test for HttpEvent::_dispatchH2Stream() dispatching each H2
// stream on a fresh, unconnected HttpRequest (one physical connection
// multiplexes many streams, each needing independent state). That temporary
// object's `slots` (from netplus::con) is always empty, so it has no way to
// answer "what's the real peer address of this connection" on its own --
// which broke any reverse proxy building X-Forwarded-For from it: the
// header ended up present-but-EMPTY on every H2 request, which is exactly
// what tripped Home Assistant's own trusted-proxy validation into a raw 400
// (aiohttp's ip_address("") parse fails) for any browser using HTTP/2 to a
// proxyplus-fronted Home Assistant instance, while HTTP/1.1 clients worked
// fine. The fix stashes the real address (known to the dispatcher, which
// still has the actual connection in scope) on the per-stream request via
// HttpRequest::setPeerAddress()/getPeerAddress().
//
// Drives a real H2 connection over a real loopback TCP socket (raw frames,
// not mocks) so the dispatcher code under test is the real thing.

#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <vector>

#include <netplus/socket.h>

#include "hpack.h"
#include "http.h"
#include "httpd.h"
#include "raw_tcp_client.h"

using namespace libhttppp;

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; }
}

constexpr uint8_t FRAME_DATA = 0x00;
constexpr uint8_t FRAME_HEADERS = 0x01;
constexpr uint8_t FRAME_SETTINGS = 0x04;
constexpr uint8_t FLAG_END_STREAM = 0x01;
constexpr uint8_t FLAG_END_HEADERS = 0x04;
constexpr size_t FRAME_HEADER_LEN = 9;
constexpr char CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";

void appendFrameHeader(std::vector<char> &out, uint32_t len, uint8_t type,
                        uint8_t flags, uint32_t streamId) {
    out.push_back(static_cast<char>((len >> 16) & 0xFF));
    out.push_back(static_cast<char>((len >> 8) & 0xFF));
    out.push_back(static_cast<char>(len & 0xFF));
    out.push_back(static_cast<char>(type));
    out.push_back(static_cast<char>(flags));
    out.push_back(static_cast<char>((streamId >> 24) & 0x7F));
    out.push_back(static_cast<char>((streamId >> 16) & 0xFF));
    out.push_back(static_cast<char>((streamId >> 8) & 0xFF));
    out.push_back(static_cast<char>(streamId & 0xFF));
}

// GET /peer-addr -> echoes HttpRequest::getPeerAddress() back as the
// x-peer-addr response header (empty if unset, so a broken build shows up
// as a present-but-empty header, the exact real-world failure shape).
class PeerAddrServer : public HttpEvent {
public:
    explicit PeerAddrServer(std::vector<netplus::socket *> serversocket)
        : HttpEvent(serversocket) {}

    void RequestEvent(HttpRequest &req, const int, ULONG_PTR) override {
        HttpResponse res;
        res.setContentType("text/plain");
        res.setHeaderData("x-peer-addr")->push_back(req.getPeerAddress());
        res.send(req, std::string("ok"), 2);
    }
};

struct RunningServer {
    std::unique_ptr<netplus::tcp> sock;
    std::unique_ptr<PeerAddrServer> server;
    std::thread thread;
};

RunningServer startServer(int &port) {
    RunningServer rs;
    rs.sock = std::make_unique<netplus::tcp>("127.0.0.1", 0, 16, -1);
    rs.sock->bind();
    port = rawtcp::boundPort(*rs.sock);
    rs.sock->listen();

    rs.server = std::make_unique<PeerAddrServer>(std::vector<netplus::socket *>{rs.sock.get()});
    netplus::event::Running = true;
    PeerAddrServer *serverPtr = rs.server.get();
    rs.thread = std::thread([serverPtr] { serverPtr->runEventloop(); });
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    return rs;
}

void stopServer(RunningServer &rs) {
    netplus::event::requestStop();
    if (rs.thread.joinable()) rs.thread.join();
}

// Connects over real loopback TCP, speaks H2 with prior knowledge (preface
// + empty SETTINGS), sends one GET /peer-addr HEADERS frame, and decodes
// the response HEADERS frame back via HPACK to read x-peer-addr.
std::string requestPeerAddrOverH2(int port) {
    netplus::tcp sock;
    rawtcp::connectLocal(sock, port);

    std::vector<char> req(CLIENT_PREFACE, CLIENT_PREFACE + sizeof(CLIENT_PREFACE) - 1);
    appendFrameHeader(req, 0, FRAME_SETTINGS, 0, 0); // empty client SETTINGS

    std::string hpack_out = hpack::Encoder::encodeRequestHeaders(
        "GET", "/peer-addr", "http", "127.0.0.1:" + std::to_string(port));
    appendFrameHeader(req, static_cast<uint32_t>(hpack_out.size()), FRAME_HEADERS,
                       FLAG_END_HEADERS | FLAG_END_STREAM, /*streamId=*/1);
    req.insert(req.end(), hpack_out.begin(), hpack_out.end());

    rawtcp::sendAllOrThrow(sock, req.data(), req.size());

    std::vector<char> buf;
    std::string headersPayload;
    bool sawEndStream = false;
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
    while (!sawEndStream && std::chrono::steady_clock::now() < deadline) {
        netplus::buffer rbuf(4096);
        size_t n;
        try { n = sock.recvData(rbuf); } catch (netplus::NetException &) { break; }
        if (n == 0) break;
        buf.insert(buf.end(), rbuf.data.buf, rbuf.data.buf + n);

        size_t off = 0;
        while (off + FRAME_HEADER_LEN <= buf.size()) {
            uint32_t len = (static_cast<uint8_t>(buf[off]) << 16) |
                           (static_cast<uint8_t>(buf[off + 1]) << 8) |
                           static_cast<uint8_t>(buf[off + 2]);
            uint8_t type = static_cast<uint8_t>(buf[off + 3]);
            uint8_t flags = static_cast<uint8_t>(buf[off + 4]);
            if (off + FRAME_HEADER_LEN + len > buf.size())
                break; // incomplete frame, wait for more bytes
            if (type == FRAME_HEADERS) {
                headersPayload.assign(buf.begin() + static_cast<ptrdiff_t>(off + FRAME_HEADER_LEN),
                                       buf.begin() + static_cast<ptrdiff_t>(off + FRAME_HEADER_LEN + len));
            }
            if ((type == FRAME_HEADERS || type == FRAME_DATA) && (flags & FLAG_END_STREAM)) {
                sawEndStream = true;
            }
            off += FRAME_HEADER_LEN + len;
        }
        buf.erase(buf.begin(), buf.begin() + off);
    }
    sock.close();

    if (!sawEndStream || headersPayload.empty())
        throw std::runtime_error("did not receive a complete H2 response");

    auto fields = hpack::Decoder::decodeStateless(
        reinterpret_cast<const uint8_t *>(headersPayload.data()), headersPayload.size());
    for (auto &f : fields) {
        if (f.name == "x-peer-addr") return f.value;
    }
    return std::string("<header not present>");
}

} // namespace

int main() {
    std::cout << "=== HTTP/2 Per-Stream Peer Address Test ===" << std::endl;
    int rc = 0;

    try {
        int port = 0;
        RunningServer rs = startServer(port);
        std::cout << "Server listening on 127.0.0.1:" << port << std::endl;

        std::string peerAddr = requestPeerAddrOverH2(port);
        stopServer(rs);

        std::cout << "  x-peer-addr seen: \"" << peerAddr << "\"" << std::endl;
        check(peerAddr == "127.0.0.1",
              "H2 per-stream request reports the real loopback peer address, "
              "not an empty/missing one");

    } catch (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;
}