diff --git a/README.md b/README.md index 6eb5ae2..5661bf3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Http11Probe is a compliance and security tester for HTTP/1.1 servers. It throws malformed, ambiguous, and oversized requests at a server over raw TCP sockets and checks how it responds against what RFC 9110 and RFC 9112 actually require. These are the awkward cases like bare LF line endings, obsolete line folding, CL/TE request smuggling, chunk-framing tricks, oversized headers, and NUL bytes, where a strict parser and a lenient one start to disagree. -The same 215 tests run against 41 reference servers written in 12 languages, from Nginx, Apache, and Envoy to Kestrel, Gin, Actix, and the built-in servers in Node, Bun, and Deno. Every result is scored against the MUST/SHOULD/MAY wording in the spec and marked **Pass**, **Fail**, or **Warn**. A Warn just means the RFC allows both the strict and the lenient behavior, so neither one is wrong. +The same 215 tests run against 42 reference servers written in 12 languages, from Nginx, Apache, and Envoy to Kestrel, Gin, Actix, and the built-in servers in Node, Bun, and Deno. Every result is scored against the MUST/SHOULD/MAY wording in the spec and marked **Pass**, **Fail**, or **Warn**. A Warn just means the RFC allows both the strict and the lenient behavior, so neither one is wrong. You'll find the full documentation, a per-test glossary with RFC citations, and the live results matrix for every server at [http-probe.com](https://www.http-probe.com/). diff --git a/docs/content/servers/libuvcpp.md b/docs/content/servers/libuvcpp.md new file mode 100644 index 0000000..83c141a --- /dev/null +++ b/docs/content/servers/libuvcpp.md @@ -0,0 +1,216 @@ +--- +title: "libuvcpp" +description: "libuvcpp (C++) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling." +toc: true +breadcrumbs: false +--- + +**Language:** C++ · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/LibuvcppServer) + +A C++11 HTTP framework built on libuv's event loop, serving through `uvcpp_web_app` -- the library's high-level server, which owns routing, the HEAD-to-GET fallback and the automatic OPTIONS answer. Built against the pinned v1.6.2 linux-x64 release package, whose shared object already contains libuv, OpenSSL, nghttp2, ngtcp2 and zlib; the sha256 in the Dockerfile is what fixes which bytes get compiled. 1.6.2 is the first release that rejects a missing, duplicated or invalid Host header (RFC 9112 section 3.2) and an all-blank Transfer-Encoding (RFC 9112 section 6.1); it scores 157/159 where 1.6.1 scores 149/159. + +## Dockerfile + +```dockerfile +# libuvcpp entry for Http11Probe. +# +# The build downloads the official v1.6.2 linux-x64 release package and compiles +# server.cpp against it -- the same artifact a user gets from the GitHub release. +# That package's shared object already carries libuv, OpenSSL, nghttp2, ngtcp2 +# and zlib, so the runtime image needs no HTTP library and no toolchain. +# +# 1.6.2 is the first release whose parser answers 400 to an HTTP/1.1 request with +# no Host header, with more than one, or with an invalid one (RFC 9112 section +# 3.2), and to an all-blank Transfer-Encoding (RFC 9112 section 6.1) -- the +# latter previously fell through to Content-Length framing while keeping the +# connection alive. Entries built against 1.6.1 or earlier report 149/159 on the +# probe; 1.6.2 reports 157/159. +# +# The download is pinned by sha256 rather than by tag: a release tag can be moved +# and its assets re-uploaded, so the hash is what fixes which bytes get built. +# The transfer is forced over HTTPS, redirects included, rather than left to +# curl's default of following a redirect into whatever scheme it names. +# +# Both stages are ubuntu:24.04 (glibc 2.39; the release package's floor is +# 2.34), so the compiler that links the binary and the libc that runs it are the +# same ones. Alpine is not an option: the package is glibc-linked. + +FROM ubuntu:24.04 AS build + +ARG LIBUVCPP_VERSION=1.6.2 +ARG LIBUVCPP_SHA256=636f07346314d5f5b8fc3dbae34861a6b4441c337fc24a632011c0d7ca72d4fe + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl g++ unzip \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +RUN curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \ + --retry 3 --retry-connrefused -o libuvcpp.zip \ + "https://github.com/Antruly/libuvcpp/releases/download/v${LIBUVCPP_VERSION}/libuvcpp-${LIBUVCPP_VERSION}-linux-x64.zip" \ + && echo "${LIBUVCPP_SHA256} libuvcpp.zip" | sha256sum -c - \ + && unzip -q libuvcpp.zip -d /opt \ + && rm -f libuvcpp.zip + +COPY src/Servers/LibuvcppServer/server.cpp . +RUN g++ -std=c++11 -O2 -DNDEBUG -o /probe-server server.cpp \ + -I/opt/libuvcpp-1.6.2-linux-x64/include \ + -L/opt/libuvcpp-1.6.2-linux-x64/lib \ + -luvcpp -Wl,-rpath,/opt/libuvcpp-1.6.2-linux-x64/lib -pthread + +FROM ubuntu:24.04 + +# g++ links the C++ runtime dynamically, so the runtime layer needs it. +RUN apt-get update \ + && apt-get install -y --no-install-recommends libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /opt/libuvcpp-1.6.2-linux-x64/lib/libuvcpp.so \ + /opt/libuvcpp-1.6.2-linux-x64/lib/libuvcpp.so +COPY --from=build /probe-server /usr/local/bin/probe-server + +USER nobody +ENTRYPOINT ["probe-server", "8080"] +``` + +## Source — `server.cpp` + +```cpp +// libuvcpp entry for Http11Probe. +// +// Three endpoints, per docs/content/add-a-framework: `/` (baseline, and body +// echo on POST), `/echo` (the headers the server received) and `/cookie` (the +// cookies the server parsed). Everything goes through `uvcpp_web_app`, the +// library's documented high-level server. + +#include +#include + +#include +#include +#include +#include +#include + +using namespace uvcpp; + +namespace { + +typedef std::vector > cookie_list; + +std::string cookies_to_lines(const cookie_list& cs) { + std::string out; + for (size_t i = 0; i < cs.size(); ++i) { + out += cs[i].first; + out += '='; + out += cs[i].second; + out += '\n'; + } + return out; +} + +// The baseline endpoint: 200 for GET, and the request body echoed back for +// POST. +void root_get(uvcpp_web_request&, uvcpp_web_response& resp, uvcpp_web_next) { + resp.text("OK"); + resp.end(); +} + +void root_post(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + const char* p = req.body_data(); + if (p == nullptr) { + resp.text(std::string()); + } else { + resp.body(p, req.body_size(), "text/plain"); + } + resp.end(); +} + +// One "name: value" line per header, in the order the parser recorded them. +void echo(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + std::string out; + const http_headers& hs = req.headers(); + for (size_t i = 0; i < hs.size(); ++i) { + out += hs[i].name; + out += ": "; + out += hs[i].value; + out += '\n'; + } + resp.text(out); + resp.end(); +} + +// What the library's own cookie parser made of the Cookie header. +void cookie(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + resp.text(cookies_to_lines(web_parse_cookies(req.header("cookie")))); + resp.end(); +} + +} // namespace + +int main(int argc, char** argv) { + const int port = argc > 1 ? std::atoi(argv[1]) : 8080; + + uvcpp_web_app app; + + // `/*path` is the catch-all; the framework's own HEAD-to-GET fallback and + // automatic OPTIONS answer the other methods, so neither is registered here. + app.get("/", root_get); + app.post("/", root_post); + app.get("/echo", echo); + app.post("/echo", echo); + app.get("/cookie", cookie); + app.post("/cookie", cookie); + app.get("/*path", root_get); + app.post("/*path", root_post); + + // Everything else is the framework's default: 0.0.0.0, port 8080 unless + // overridden, and the documented HEAD/OPTIONS behaviour above. + app.set_port(port); + + const int rc = app.start_background(); + if (rc != 0) { + std::fprintf(stderr, "listen on %d failed: %d\n", port, rc); + return 1; + } + app.join(); + return 0; +} +``` + +## Test Results + +

Loading results...

+ +### Compliance + +
+ +### Smuggling + +
+ +### Malformed Input + +
+ +### Caching + +
+ +### Cookies + +
+ + + + diff --git a/src/Servers/LibuvcppServer/Dockerfile b/src/Servers/LibuvcppServer/Dockerfile new file mode 100644 index 0000000..5bac166 --- /dev/null +++ b/src/Servers/LibuvcppServer/Dockerfile @@ -0,0 +1,60 @@ +# libuvcpp entry for Http11Probe. +# +# The build downloads the official v1.6.2 linux-x64 release package and compiles +# server.cpp against it -- the same artifact a user gets from the GitHub release. +# That package's shared object already carries libuv, OpenSSL, nghttp2, ngtcp2 +# and zlib, so the runtime image needs no HTTP library and no toolchain. +# +# 1.6.2 is the first release whose parser answers 400 to an HTTP/1.1 request with +# no Host header, with more than one, or with an invalid one (RFC 9112 section +# 3.2), and to an all-blank Transfer-Encoding (RFC 9112 section 6.1) -- the +# latter previously fell through to Content-Length framing while keeping the +# connection alive. Entries built against 1.6.1 or earlier report 149/159 on the +# probe; 1.6.2 reports 157/159. +# +# The download is pinned by sha256 rather than by tag: a release tag can be moved +# and its assets re-uploaded, so the hash is what fixes which bytes get built. +# The transfer is forced over HTTPS, redirects included, rather than left to +# curl's default of following a redirect into whatever scheme it names. +# +# Both stages are ubuntu:24.04 (glibc 2.39; the release package's floor is +# 2.34), so the compiler that links the binary and the libc that runs it are the +# same ones. Alpine is not an option: the package is glibc-linked. + +FROM ubuntu:24.04 AS build + +ARG LIBUVCPP_VERSION=1.6.2 +ARG LIBUVCPP_SHA256=636f07346314d5f5b8fc3dbae34861a6b4441c337fc24a632011c0d7ca72d4fe + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl g++ unzip \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +RUN curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \ + --retry 3 --retry-connrefused -o libuvcpp.zip \ + "https://github.com/Antruly/libuvcpp/releases/download/v${LIBUVCPP_VERSION}/libuvcpp-${LIBUVCPP_VERSION}-linux-x64.zip" \ + && echo "${LIBUVCPP_SHA256} libuvcpp.zip" | sha256sum -c - \ + && unzip -q libuvcpp.zip -d /opt \ + && rm -f libuvcpp.zip + +COPY src/Servers/LibuvcppServer/server.cpp . +RUN g++ -std=c++11 -O2 -DNDEBUG -o /probe-server server.cpp \ + -I/opt/libuvcpp-1.6.2-linux-x64/include \ + -L/opt/libuvcpp-1.6.2-linux-x64/lib \ + -luvcpp -Wl,-rpath,/opt/libuvcpp-1.6.2-linux-x64/lib -pthread + +FROM ubuntu:24.04 + +# g++ links the C++ runtime dynamically, so the runtime layer needs it. +RUN apt-get update \ + && apt-get install -y --no-install-recommends libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /opt/libuvcpp-1.6.2-linux-x64/lib/libuvcpp.so \ + /opt/libuvcpp-1.6.2-linux-x64/lib/libuvcpp.so +COPY --from=build /probe-server /usr/local/bin/probe-server + +USER nobody +ENTRYPOINT ["probe-server", "8080"] diff --git a/src/Servers/LibuvcppServer/probe.json b/src/Servers/LibuvcppServer/probe.json new file mode 100644 index 0000000..dd7871c --- /dev/null +++ b/src/Servers/LibuvcppServer/probe.json @@ -0,0 +1 @@ +{"name": "libuvcpp", "language": "C++", "repository": "https://github.com/Antruly/libuvcpp"} diff --git a/src/Servers/LibuvcppServer/server.cpp b/src/Servers/LibuvcppServer/server.cpp new file mode 100644 index 0000000..6bc03b7 --- /dev/null +++ b/src/Servers/LibuvcppServer/server.cpp @@ -0,0 +1,100 @@ +// libuvcpp entry for Http11Probe. +// +// Three endpoints, per docs/content/add-a-framework: `/` (baseline, and body +// echo on POST), `/echo` (the headers the server received) and `/cookie` (the +// cookies the server parsed). Everything goes through `uvcpp_web_app`, the +// library's documented high-level server. + +#include +#include + +#include +#include +#include +#include +#include + +using namespace uvcpp; + +namespace { + +typedef std::vector > cookie_list; + +std::string cookies_to_lines(const cookie_list& cs) { + std::string out; + for (size_t i = 0; i < cs.size(); ++i) { + out += cs[i].first; + out += '='; + out += cs[i].second; + out += '\n'; + } + return out; +} + +// The baseline endpoint: 200 for GET, and the request body echoed back for +// POST. +void root_get(uvcpp_web_request&, uvcpp_web_response& resp, uvcpp_web_next) { + resp.text("OK"); + resp.end(); +} + +void root_post(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + const char* p = req.body_data(); + if (p == nullptr) { + resp.text(std::string()); + } else { + resp.body(p, req.body_size(), "text/plain"); + } + resp.end(); +} + +// One "name: value" line per header, in the order the parser recorded them. +void echo(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + std::string out; + const http_headers& hs = req.headers(); + for (size_t i = 0; i < hs.size(); ++i) { + out += hs[i].name; + out += ": "; + out += hs[i].value; + out += '\n'; + } + resp.text(out); + resp.end(); +} + +// What the library's own cookie parser made of the Cookie header. +void cookie(uvcpp_web_request& req, uvcpp_web_response& resp, uvcpp_web_next) { + resp.text(cookies_to_lines(web_parse_cookies(req.header("cookie")))); + resp.end(); +} + +} // namespace + +int main(int argc, char** argv) { + const int port = argc > 1 ? std::atoi(argv[1]) : 8080; + + uvcpp_web_app app; + + // `/*path` is the catch-all; the framework's own HEAD-to-GET fallback and + // automatic OPTIONS answer the other methods, so neither is registered here. + app.get("/", root_get); + app.post("/", root_post); + app.get("/echo", echo); + app.post("/echo", echo); + app.get("/cookie", cookie); + app.post("/cookie", cookie); + app.get("/*path", root_get); + app.post("/*path", root_post); + + // Everything else is the framework's default: 0.0.0.0, port 8080 unless + // overridden, and the documented HEAD/OPTIONS behaviour above. + app.set_port(port); + + const int rc = app.start_background(); + if (rc != 0) { + std::fprintf(stderr, "listen on %d failed: %d\n", port, rc); + return 1; + } + app.join(); + return 0; +}