mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
These tests exercise the squid proxies by hitting /basic-auth and /digest-auth on an external httpbin-style site. That site's identity has already moved twice (httpbin.org -> httpcan.org, per #2300) chasing uptime, and httpcan.org itself is now down (Cloudflare 502 from its origin), failing CI with no code change involved. Adds two containers to the existing squid docker-compose stack instead: go-httpbin (mccutchen/go-httpbin) as the backend, and an nginx sidecar in front of it under the single "httpbin" hostname so both the NoSSL tests (port 80) and the SSL tests, which CONNECT-tunnel through the proxy to port 443, resolve the same name -- go-httpbin only listens on one port at a time, so it can't serve both protocols itself. nginx uses the repo's existing self-signed test cert; the SSL client tests already disable verification for it like other self-signed-cert tests in this suite. go-httpbin was picked over the more feature-complete kennethreitz/httpbin after finding the latter accepts a wrong digest-auth username as long as the password matches -- confirmed with a direct curl against the container, unrelated to anything in this repo. go-httpbin correctly rejects both. The trade-off is losing SHA-512 digest-auth coverage here, since go-httpbin only implements MD5 and SHA-256; nothing else in the suite exercises SHA-512 digest auth against a live server. Response body assertions are adjusted to go-httpbin's actual JSON shape (an added "authorized" field, no "algorithm" field), and the domain changes from httpcan.org to the self-hosted "httpbin". This only affects 'make proxy'/'make proxy_mbedtls'/'make proxy_wolfssl' and the Proxy Test CI workflow -- the default 'make' target is untouched.
377 lines
10 KiB
C++
377 lines
10 KiB
C++
#include <chrono>
|
|
#include <future>
|
|
#include <gtest/gtest.h>
|
|
#include <httplib.h>
|
|
|
|
using namespace std;
|
|
using namespace httplib;
|
|
|
|
std::string normalizeJson(const std::string &json) {
|
|
std::string result;
|
|
for (char c : json) {
|
|
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { result += c; }
|
|
}
|
|
return result;
|
|
}
|
|
|
|
template <typename T> void ProxyTest(T &cli, bool basic) {
|
|
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
|
auto res = cli.Get("/get");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res->status);
|
|
}
|
|
|
|
TEST(ProxyTest, NoSSLBasic) {
|
|
Client cli("httpbingo.org");
|
|
ProxyTest(cli, true);
|
|
}
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(ProxyTest, SSLBasic) {
|
|
SSLClient cli("httpbingo.org");
|
|
ProxyTest(cli, true);
|
|
}
|
|
|
|
TEST(ProxyTest, NoSSLDigest) {
|
|
Client cli("httpbingo.org");
|
|
ProxyTest(cli, false);
|
|
}
|
|
|
|
TEST(ProxyTest, SSLDigest) {
|
|
SSLClient cli("httpbingo.org");
|
|
ProxyTest(cli, false);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
template <typename T>
|
|
void RedirectProxyText(T &cli, const char *path, bool basic) {
|
|
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
|
if (basic) {
|
|
cli.set_proxy_basic_auth("hello", "world");
|
|
} else {
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
cli.set_proxy_digest_auth("hello", "world");
|
|
#endif
|
|
}
|
|
cli.set_follow_location(true);
|
|
|
|
auto res = cli.Get(path);
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
|
|
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
|
Client cli("httpbingo.org");
|
|
RedirectProxyText(cli, "/redirect/2", true);
|
|
}
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
|
Client cli("httpbingo.org");
|
|
RedirectProxyText(cli, "/redirect/2", false);
|
|
}
|
|
|
|
TEST(RedirectTest, HTTPBinSSLBasic) {
|
|
SSLClient cli("httpbingo.org");
|
|
RedirectProxyText(cli, "/redirect/2", true);
|
|
}
|
|
|
|
TEST(RedirectTest, HTTPBinSSLDigest) {
|
|
SSLClient cli("httpbingo.org");
|
|
RedirectProxyText(cli, "/redirect/2", false);
|
|
}
|
|
#endif
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(RedirectTest, YouTubeNoSSLBasic) {
|
|
Client cli("youtube.com");
|
|
RedirectProxyText(cli, "/", true);
|
|
}
|
|
|
|
TEST(RedirectTest, YouTubeNoSSLDigest) {
|
|
Client cli("youtube.com");
|
|
RedirectProxyText(cli, "/", false);
|
|
}
|
|
|
|
TEST(RedirectTest, YouTubeSSLBasic) {
|
|
SSLClient cli("youtube.com");
|
|
RedirectProxyText(cli, "/", true);
|
|
}
|
|
|
|
TEST(RedirectTest, YouTubeSSLDigest) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(3));
|
|
SSLClient cli("youtube.com");
|
|
RedirectProxyText(cli, "/", false);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(RedirectTest, TLSVerificationOnProxyRedirect) {
|
|
// Untrusted HTTPS server with self-signed cert
|
|
SSLServer untrusted_svr("cert.pem", "key.pem");
|
|
untrusted_svr.Get("/", [](const Request &, Response &res) {
|
|
res.set_content("MITM'd", "text/plain");
|
|
});
|
|
|
|
auto untrusted_port = untrusted_svr.bind_to_any_port("0.0.0.0");
|
|
auto t1 = thread([&]() { untrusted_svr.listen_after_bind(); });
|
|
auto se1 = detail::scope_exit([&] {
|
|
untrusted_svr.stop();
|
|
t1.join();
|
|
});
|
|
|
|
// HTTP server that redirects to the untrusted HTTPS server
|
|
// Use host.docker.internal so the proxy container can reach the host
|
|
Server redirect_svr;
|
|
redirect_svr.Get("/", [&](const Request &, Response &res) {
|
|
res.set_redirect(
|
|
"https://host.docker.internal:" + to_string(untrusted_port) + "/");
|
|
});
|
|
|
|
auto redirect_port = redirect_svr.bind_to_any_port("0.0.0.0");
|
|
auto t2 = thread([&]() { redirect_svr.listen_after_bind(); });
|
|
auto se2 = detail::scope_exit([&] {
|
|
redirect_svr.stop();
|
|
t2.join();
|
|
});
|
|
|
|
// Wait until servers are up
|
|
untrusted_svr.wait_until_ready();
|
|
redirect_svr.wait_until_ready();
|
|
|
|
// Client with proxy + follow_location, verification ON (default)
|
|
Client cli("host.docker.internal", redirect_port);
|
|
cli.set_proxy("localhost", 3128);
|
|
cli.set_proxy_basic_auth("hello", "world");
|
|
cli.set_follow_location(true);
|
|
|
|
auto res = cli.Get("/");
|
|
|
|
// Self-signed cert must be rejected
|
|
ASSERT_TRUE(res == nullptr);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|
cli.set_proxy("localhost", 3128);
|
|
cli.set_proxy_basic_auth("hello", "world");
|
|
|
|
{
|
|
auto res = cli.Get("/basic-auth/hello/world");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
|
|
{
|
|
auto res =
|
|
cli.Get("/basic-auth/hello/world",
|
|
Headers{make_basic_authentication_header("hello", "world")});
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
|
"\"authorized\":true}\n"),
|
|
normalizeJson(res->body));
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
|
|
{
|
|
cli.set_basic_auth("hello", "world");
|
|
auto res = cli.Get("/basic-auth/hello/world");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
|
"\"authorized\":true}\n"),
|
|
normalizeJson(res->body));
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
|
|
{
|
|
cli.set_basic_auth("hello", "bad");
|
|
auto res = cli.Get("/basic-auth/hello/world");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
|
|
{
|
|
cli.set_basic_auth("bad", "world");
|
|
auto res = cli.Get("/basic-auth/hello/world");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
}
|
|
|
|
TEST(BaseAuthTest, NoSSL) {
|
|
Client cli("httpbin");
|
|
BaseAuthTestFromHTTPWatch(cli);
|
|
}
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(BaseAuthTest, SSL) {
|
|
SSLClient cli("httpbin");
|
|
cli.enable_server_certificate_verification(false);
|
|
BaseAuthTestFromHTTPWatch(cli);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
|
cli.set_proxy("localhost", 3129);
|
|
cli.set_proxy_digest_auth("hello", "world");
|
|
|
|
{
|
|
auto res = cli.Get("/digest-auth/auth/hello/world");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
|
|
{
|
|
// go-httpbin (the "httpbin" test double) only implements MD5 and
|
|
// SHA-256 for digest auth, so SHA-256 is as far as this can exercise
|
|
// the client's digest-auth algorithm selection end-to-end.
|
|
std::vector<std::string> paths = {
|
|
"/digest-auth/auth/hello/world/MD5",
|
|
"/digest-auth/auth/hello/world/SHA-256",
|
|
};
|
|
|
|
cli.set_digest_auth("hello", "world");
|
|
for (auto path : paths) {
|
|
auto res = cli.Get(path.c_str());
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
|
"\"authorized\":true}\n"),
|
|
normalizeJson(res->body));
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
|
|
cli.set_digest_auth("hello", "bad");
|
|
for (auto path : paths) {
|
|
auto res = cli.Get(path.c_str());
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
|
|
cli.set_digest_auth("bad", "world");
|
|
for (auto path : paths) {
|
|
auto res = cli.Get(path.c_str());
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(DigestAuthTest, SSL) {
|
|
SSLClient cli("httpbin");
|
|
cli.enable_server_certificate_verification(false);
|
|
DigestAuthTestFromHTTPWatch(cli);
|
|
}
|
|
|
|
TEST(DigestAuthTest, NoSSL) {
|
|
Client cli("httpbin");
|
|
DigestAuthTestFromHTTPWatch(cli);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
|
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
|
if (basic) {
|
|
cli.set_proxy_basic_auth("hello", "world");
|
|
} else {
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
cli.set_proxy_digest_auth("hello", "world");
|
|
#endif
|
|
}
|
|
|
|
cli.set_follow_location(true);
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
cli.set_digest_auth("hello", "world");
|
|
#endif
|
|
|
|
{
|
|
auto res = cli.Get("/get");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
{
|
|
auto res = cli.Get("/redirect/2");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
|
|
{
|
|
std::vector<std::string> paths = {
|
|
"/digest-auth/auth/hello/world/MD5",
|
|
"/digest-auth/auth/hello/world/SHA-256",
|
|
};
|
|
|
|
for (auto path : paths) {
|
|
auto res = cli.Get(path.c_str());
|
|
ASSERT_TRUE(res != nullptr);
|
|
auto body = normalizeJson(res->body);
|
|
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
|
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
}
|
|
|
|
{
|
|
int count = 10;
|
|
while (count--) {
|
|
auto res = cli.Get("/get");
|
|
ASSERT_TRUE(res != nullptr);
|
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(KeepAliveTest, NoSSLWithBasic) {
|
|
Client cli("httpbingo.org");
|
|
KeepAliveTest(cli, true);
|
|
}
|
|
|
|
TEST(KeepAliveTest, SSLWithBasic) {
|
|
SSLClient cli("httpbingo.org");
|
|
KeepAliveTest(cli, true);
|
|
}
|
|
|
|
TEST(KeepAliveTest, NoSSLWithDigest) {
|
|
Client cli("httpbingo.org");
|
|
KeepAliveTest(cli, false);
|
|
}
|
|
|
|
TEST(KeepAliveTest, SSLWithDigest) {
|
|
SSLClient cli("httpbingo.org");
|
|
KeepAliveTest(cli, false);
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
TEST(ProxyTest, SSLOpenStream) {
|
|
SSLClient cli("httpbingo.org");
|
|
cli.set_proxy("localhost", 3128);
|
|
cli.set_proxy_basic_auth("hello", "world");
|
|
|
|
auto handle = cli.open_stream("GET", "/get");
|
|
ASSERT_TRUE(handle.response);
|
|
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
|
|
|
std::string body;
|
|
char buf[8192];
|
|
ssize_t n;
|
|
while ((n = handle.read(buf, sizeof(buf))) > 0) {
|
|
body.append(buf, static_cast<size_t>(n));
|
|
}
|
|
EXPECT_FALSE(body.empty());
|
|
}
|
|
#endif
|