Loading test/test_http_request_body_streaming.cpp +93 −21 Changes for test/test_http_request_body_streaming.cpp: 93 added lines, 21 removed lines. Original line number Diff line number Diff line // End-to-end test for streamed request-body relaying through a real HttpProxyController: // client -> proxyplus -> fake upstream, checking the upstream receives exactly the bytes the // client sent (sha256) for HTTP/1.1 (Content-Length and chunked) and HTTP/2 (h2c, declared and // undeclared length), and that process.max_request_body_mb answers 413 both up front (declared // Content-Length) and mid-upload (no declared length). // client sent (sha256) over every protocol the proxy listens on -- HTTP/1.1 (Content-Length and // chunked), HTTP/2 via TLS+ALPN (HttpClient) and cleartext h2c (raw client), and HTTP/3 over // QUIC -- and that process.max_request_body_mb answers 413 both up front (declared // Content-Length) and mid-upload (no declared length), after which the client still works. // // Bare assert() is compiled out here (RelWithDebInfo passes -DNDEBUG), hence check(). Loading @@ -21,6 +22,7 @@ #include <netplus/exception.h> #include <netplus/random.h> #include <netplus/socket.h> #include <netplus/utils/certgen.h> #include <httppp/hpack.h> #include <httppp/http.h> Loading Loading @@ -284,6 +286,52 @@ std::string headerOf(const libhttppp::HttpResponse *res, const std::string &key) return hd && hd->getfirstValue() ? hd->getfirstValue()->getvalue() : std::string(); } libhttppp::HttpClient::BodySource sourceOf(const std::vector<char> &data); // The same checks over whichever protocol `client` speaks: 6MB with and without a declared // length reaches the upstream intact; 20MB without a declared length is cut off at the limit // with 413 mid-upload; and the client can carry on afterwards. void clientUploads(libhttppp::HttpClient &client, const std::string &proto, size_t okSize, size_t tooBig) { for (bool known : {true, false}) { std::string label = proto + (known ? " known length 6MB" : " unknown length 6MB"); try { std::vector<char> data = randomBuffer(okSize); libhttppp::HttpRequest req; req.setRequestURL("/upload"); std::vector<char> resp = client.RequestStream( POSTREQUEST, req, sourceOf(data), known ? int64_t(data.size()) : -1); check(client.lastStatusCode() == 200, label + ": 200"); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()), label + ": upstream got every byte"); check(headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), label + ": sha256 matches at upstream"); } catch (std::exception &e) { check(false, label + ": threw " + e.what()); } } try { std::vector<char> data = randomBuffer(tooBig); libhttppp::HttpRequest req; req.setRequestURL("/upload"); client.RequestStream(POSTREQUEST, req, sourceOf(data), -1); check(client.lastStatusCode() == 413, proto + " undeclared length over limit: 413 mid-upload"); } catch (std::exception &e) { check(false, proto + " over limit: threw " + std::string(e.what())); } try { std::vector<char> data = randomBuffer(okSize); libhttppp::HttpRequest req; req.setRequestURL("/upload"); client.RequestStream(POSTREQUEST, req, sourceOf(data), int64_t(data.size())); check(client.lastStatusCode() == 200 && headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), proto + " same client still works after the 413"); } catch (std::exception &e) { check(false, proto + " after 413: threw " + std::string(e.what())); } } libhttppp::HttpClient::BodySource sourceOf(const std::vector<char> &data) { auto off = std::make_shared<size_t>(0); return [&data, off](char *buf, size_t len) -> size_t { Loading Loading @@ -315,6 +363,29 @@ int main() { const int proxyPort = boundPort(*proxySock); proxySock->listen(); // TLS (h2 via ALPN) and QUIC (h3) listeners on the same proxy, with a throwaway // self-signed certificate (the clients here don't verify). netplus::CertGenConfig certCfg; certCfg.commonName = "127.0.0.1"; certCfg.subjectAltNames = {"127.0.0.1", "localhost"}; netplus::CertKeyPair kp; if (!netplus::generateSelfSignedCert(certCfg, kp) || !netplus::writePemFile(std::string(tmpl) + "/cert.pem", kp.certPem) || !netplus::writePemFile(std::string(tmpl) + "/key.pem", kp.keyPem)) { std::cerr << "certificate generation failed" << std::endl; return 1; } netplus::ssl::CertificateBundle bundle; if (!bundle.loadFromFile(std::string(tmpl) + "/cert.pem", std::string(tmpl) + "/key.pem")) { std::cerr << "loading the generated certificate failed" << std::endl; return 1; } std::map<std::string, netplus::ssl::CertificateBundle> certs{{"127.0.0.1", bundle}, {"localhost", bundle}}; constexpr int kTlsPort = 19661, kQuicPort = 19662; auto tlsSock = std::make_unique<netplus::ssl>(certs, "127.0.0.1", kTlsPort, 16, -1); auto quicSock = std::make_unique<netplus::quic>(certs, "127.0.0.1", kQuicPort, 64, -1); store::ConfigStore configStore(tmpl); store::HttpRouteRecord route; route.id = "body-stream-test"; Loading @@ -330,7 +401,8 @@ int main() { http::UpstreamConnectionPool pool(8, 60); Upstream upstream({upSock.get()}); httpproxy::HttpProxyController proxy({proxySock.get()}, "test", /*tlsActive=*/false, httpproxy::HttpProxyController proxy({proxySock.get(), tlsSock.get(), quicSock.get()}, "test", /*tlsActive=*/false, configStore, authService, pluginManager, loadBalancer, pool, nullptr, 0, 30, 256, 256, kLimitMb); netplus::event::Running = true; Loading @@ -343,23 +415,7 @@ int main() { { libhttppp::HttpClient client( libhttppp::HttpUrl("http://127.0.0.1:" + std::to_string(proxyPort) + "/"), 1, 30); for (bool known : {true, false}) { std::string label = known ? "H1 content-length 6MB" : "H1 chunked 6MB"; try { std::vector<char> data = randomBuffer(kOk); libhttppp::HttpRequest req; req.setRequestURL("/upload"); std::vector<char> resp = client.RequestStream( POSTREQUEST, req, sourceOf(data), known ? int64_t(data.size()) : -1); check(client.lastStatusCode() == 200, label + ": 200"); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()), label + ": upstream got every byte"); check(headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), label + ": sha256 matches at upstream"); } catch (std::exception &e) { check(false, label + ": threw " + e.what()); } } clientUploads(client, "H1", kOk, kTooBig); } { // Declared length over the limit: 413 before a single body byte is read. Loading @@ -381,6 +437,22 @@ int main() { sock.close(); } std::cout << "-- HTTP/2 (TLS, HttpClient) --" << std::endl; { libhttppp::HttpClient client( libhttppp::HttpUrl("https://127.0.0.1:" + std::to_string(kTlsPort) + "/"), 2, 30); clientUploads(client, "H2/TLS", kOk, kTooBig); } std::cout << "-- HTTP/3 --" << std::endl; { libhttppp::HttpClient client( libhttppp::HttpUrl("https://127.0.0.1:" + std::to_string(kQuicPort) + "/", /*http3=*/true), 3, 30); clientUploads(client, "H3", kOk, kTooBig); } std::cout << "-- HTTP/2 (h2c) --" << std::endl; { H2Uploader client(proxyPort); Loading Loading
test/test_http_request_body_streaming.cpp +93 −21 Changes for test/test_http_request_body_streaming.cpp: 93 added lines, 21 removed lines. Original line number Diff line number Diff line // End-to-end test for streamed request-body relaying through a real HttpProxyController: // client -> proxyplus -> fake upstream, checking the upstream receives exactly the bytes the // client sent (sha256) for HTTP/1.1 (Content-Length and chunked) and HTTP/2 (h2c, declared and // undeclared length), and that process.max_request_body_mb answers 413 both up front (declared // Content-Length) and mid-upload (no declared length). // client sent (sha256) over every protocol the proxy listens on -- HTTP/1.1 (Content-Length and // chunked), HTTP/2 via TLS+ALPN (HttpClient) and cleartext h2c (raw client), and HTTP/3 over // QUIC -- and that process.max_request_body_mb answers 413 both up front (declared // Content-Length) and mid-upload (no declared length), after which the client still works. // // Bare assert() is compiled out here (RelWithDebInfo passes -DNDEBUG), hence check(). Loading @@ -21,6 +22,7 @@ #include <netplus/exception.h> #include <netplus/random.h> #include <netplus/socket.h> #include <netplus/utils/certgen.h> #include <httppp/hpack.h> #include <httppp/http.h> Loading Loading @@ -284,6 +286,52 @@ std::string headerOf(const libhttppp::HttpResponse *res, const std::string &key) return hd && hd->getfirstValue() ? hd->getfirstValue()->getvalue() : std::string(); } libhttppp::HttpClient::BodySource sourceOf(const std::vector<char> &data); // The same checks over whichever protocol `client` speaks: 6MB with and without a declared // length reaches the upstream intact; 20MB without a declared length is cut off at the limit // with 413 mid-upload; and the client can carry on afterwards. void clientUploads(libhttppp::HttpClient &client, const std::string &proto, size_t okSize, size_t tooBig) { for (bool known : {true, false}) { std::string label = proto + (known ? " known length 6MB" : " unknown length 6MB"); try { std::vector<char> data = randomBuffer(okSize); libhttppp::HttpRequest req; req.setRequestURL("/upload"); std::vector<char> resp = client.RequestStream( POSTREQUEST, req, sourceOf(data), known ? int64_t(data.size()) : -1); check(client.lastStatusCode() == 200, label + ": 200"); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()), label + ": upstream got every byte"); check(headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), label + ": sha256 matches at upstream"); } catch (std::exception &e) { check(false, label + ": threw " + e.what()); } } try { std::vector<char> data = randomBuffer(tooBig); libhttppp::HttpRequest req; req.setRequestURL("/upload"); client.RequestStream(POSTREQUEST, req, sourceOf(data), -1); check(client.lastStatusCode() == 413, proto + " undeclared length over limit: 413 mid-upload"); } catch (std::exception &e) { check(false, proto + " over limit: threw " + std::string(e.what())); } try { std::vector<char> data = randomBuffer(okSize); libhttppp::HttpRequest req; req.setRequestURL("/upload"); client.RequestStream(POSTREQUEST, req, sourceOf(data), int64_t(data.size())); check(client.lastStatusCode() == 200 && headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), proto + " same client still works after the 413"); } catch (std::exception &e) { check(false, proto + " after 413: threw " + std::string(e.what())); } } libhttppp::HttpClient::BodySource sourceOf(const std::vector<char> &data) { auto off = std::make_shared<size_t>(0); return [&data, off](char *buf, size_t len) -> size_t { Loading Loading @@ -315,6 +363,29 @@ int main() { const int proxyPort = boundPort(*proxySock); proxySock->listen(); // TLS (h2 via ALPN) and QUIC (h3) listeners on the same proxy, with a throwaway // self-signed certificate (the clients here don't verify). netplus::CertGenConfig certCfg; certCfg.commonName = "127.0.0.1"; certCfg.subjectAltNames = {"127.0.0.1", "localhost"}; netplus::CertKeyPair kp; if (!netplus::generateSelfSignedCert(certCfg, kp) || !netplus::writePemFile(std::string(tmpl) + "/cert.pem", kp.certPem) || !netplus::writePemFile(std::string(tmpl) + "/key.pem", kp.keyPem)) { std::cerr << "certificate generation failed" << std::endl; return 1; } netplus::ssl::CertificateBundle bundle; if (!bundle.loadFromFile(std::string(tmpl) + "/cert.pem", std::string(tmpl) + "/key.pem")) { std::cerr << "loading the generated certificate failed" << std::endl; return 1; } std::map<std::string, netplus::ssl::CertificateBundle> certs{{"127.0.0.1", bundle}, {"localhost", bundle}}; constexpr int kTlsPort = 19661, kQuicPort = 19662; auto tlsSock = std::make_unique<netplus::ssl>(certs, "127.0.0.1", kTlsPort, 16, -1); auto quicSock = std::make_unique<netplus::quic>(certs, "127.0.0.1", kQuicPort, 64, -1); store::ConfigStore configStore(tmpl); store::HttpRouteRecord route; route.id = "body-stream-test"; Loading @@ -330,7 +401,8 @@ int main() { http::UpstreamConnectionPool pool(8, 60); Upstream upstream({upSock.get()}); httpproxy::HttpProxyController proxy({proxySock.get()}, "test", /*tlsActive=*/false, httpproxy::HttpProxyController proxy({proxySock.get(), tlsSock.get(), quicSock.get()}, "test", /*tlsActive=*/false, configStore, authService, pluginManager, loadBalancer, pool, nullptr, 0, 30, 256, 256, kLimitMb); netplus::event::Running = true; Loading @@ -343,23 +415,7 @@ int main() { { libhttppp::HttpClient client( libhttppp::HttpUrl("http://127.0.0.1:" + std::to_string(proxyPort) + "/"), 1, 30); for (bool known : {true, false}) { std::string label = known ? "H1 content-length 6MB" : "H1 chunked 6MB"; try { std::vector<char> data = randomBuffer(kOk); libhttppp::HttpRequest req; req.setRequestURL("/upload"); std::vector<char> resp = client.RequestStream( POSTREQUEST, req, sourceOf(data), known ? int64_t(data.size()) : -1); check(client.lastStatusCode() == 200, label + ": 200"); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()), label + ": upstream got every byte"); check(headerOf(client.lastResponse(), "x-sha256") == sha256Hex(data), label + ": sha256 matches at upstream"); } catch (std::exception &e) { check(false, label + ": threw " + e.what()); } } clientUploads(client, "H1", kOk, kTooBig); } { // Declared length over the limit: 413 before a single body byte is read. Loading @@ -381,6 +437,22 @@ int main() { sock.close(); } std::cout << "-- HTTP/2 (TLS, HttpClient) --" << std::endl; { libhttppp::HttpClient client( libhttppp::HttpUrl("https://127.0.0.1:" + std::to_string(kTlsPort) + "/"), 2, 30); clientUploads(client, "H2/TLS", kOk, kTooBig); } std::cout << "-- HTTP/3 --" << std::endl; { libhttppp::HttpClient client( libhttppp::HttpUrl("https://127.0.0.1:" + std::to_string(kQuicPort) + "/", /*http3=*/true), 3, 30); clientUploads(client, "H3", kOk, kTooBig); } std::cout << "-- HTTP/2 (h2c) --" << std::endl; { H2Uploader client(proxyPort); Loading