Files
pip/tests/http_server/http_server_protected_test.cpp
andrey e1a24a98f7 fix: accept case-insensitive Basic scheme and guard HTTP test target
- parse the Basic auth-scheme case-insensitively (RFC 7235), matching Bearer
- register the http_server test only when pip_http_server and pip_http_client
  targets exist, so TESTS=ON without the HTTP modules does not fail to link
- add a Basic_LowercaseScheme test
2026-09-23 08:49:04 +03:00

514 lines
19 KiB
C++

#include "pihttpclient.h"
#include "pihttpservermodule.h"
#include "piliterals_string.h"
#include "piliterals_time.h"
#include "pisemaphore.h"
#include "gtest/gtest.h"
//! \~english Test server that allows access only with the "Bearer secret" authorization header.
//! \~russian Тестовый сервер, разрешающий доступ только с заголовком авторизации "Bearer secret".
class TokenServer: public PIHTTPServerProtected {
public:
PIHTTP::AuthInfo authenticate(const PIHTTP::MessageConst & request) override {
return request.headers().value(PIHTTP::Header::Authorization) == "Bearer secret"_a;
}
PIHTTP::MessageMutable accessDeniedReply(const PIHTTP::MessageConst &) override {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Unauthorized)
.addHeader(PIHTTP::Header::WWWAuthenticate, "Bearer realm=\"token\""_a);
}
};
//! \~english Test server that denies every protected request and replies 403 with body "no".
//! \~russian Тестовый сервер, отказывающий во всех защищенных запросах и отвечающий 403 с телом "no".
class ForbiddenServer: public PIHTTPServerProtected {
public:
PIHTTP::AuthInfo authenticate(const PIHTTP::MessageConst &) override { return PIHTTP::AuthInfo(); }
PIHTTP::MessageMutable accessDeniedReply(const PIHTTP::MessageConst &) override {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Forbidden).setBody(PIByteArray::fromAscii("no"));
}
};
class HttpServerProtectedTest: public ::testing::Test {
protected:
struct Reply {
PIHTTP::Code code = PIHTTP::Code::Unknown;
PIString body;
PIString www_authenticate;
PIString error;
bool finished = false;
bool transport_error = false;
};
//! \~english Performs a GET request to "url" with optional "Authorization" header value and
//! waits for the reply with a 10 seconds timeout.
//! \~russian Выполняет GET-запрос к "url" с необязательным значением заголовка "Authorization"
//! и ждет ответ с таймаутом 10 секунд.
static Reply get(const PIString & url, const PIString & auth) {
// The reply state and semaphore are heap-allocated: the callbacks run in the
// HTTP client thread pool and may outlive this function on a wait timeout.
Reply * rep = new Reply();
PISemaphore * sem = new PISemaphore();
PIHTTP::MessageMutable req;
if (auth.isNotEmpty()) req.addHeader(PIHTTP::Header::Authorization, auth);
auto client = PIHTTPClient::create(url, PIHTTP::Method::Get, req);
client->onFinish([rep, sem](const PIHTTP::MessageConst & r) {
rep->code = r.code();
rep->body = PIString::fromUTF8(r.body());
rep->www_authenticate = r.headers().value(PIHTTP::Header::WWWAuthenticate);
rep->finished = true;
sem->release();
});
client->onError([client, rep, sem](const PIHTTP::MessageConst &) {
rep->error = client->lastError();
rep->transport_error = true;
rep->finished = true;
sem->release();
});
client->start();
// The client object is owned by the HTTP client thread pool after start()
// and deleted there, so it must not be touched or deleted here.
if (sem->tryAcquire(1, 10_s)) {
Reply out = *rep;
delete rep;
delete sem;
return out;
}
// On timeout the callbacks may still run in the client thread pool,
// so "rep"/"sem" are intentionally leaked (the test fails anyway).
Reply out;
return out;
}
//! \~english Builds a Basic-scheme "Authorization" header value from "user:pass".
//! \~russian Формирует значение заголовка "Authorization" схемы Basic из "user:pass".
static PIString basicAuth(const char * user_pass) {
return "Basic %1"_a.arg(PIString::fromAscii(PIByteArray::fromAscii(user_pass).toBase64()));
}
//! \~english Starts listening on the first free port from 18461 and keeps the server
//! for the teardown. Returns \c false if no port is available.
//! \~russian Запускает прослушивание на первом свободном порту от 18461 и сохраняет сервер
//! для завершения теста. Возвращает \c false, если свободный порт не найден.
bool start(PIHTTPServer * s) {
server = s;
for (port = 18461; port < 18561; ++port) {
if (s->listenAll((ushort)port)) return true;
}
port = -1;
return false;
}
PIString url(const char * path) const { return "http://127.0.0.1:%1"_a.arg(port) + path; }
void TearDown() override {
if (server) {
server->stop();
delete server;
server = nullptr;
}
}
PIHTTPServer * server = nullptr;
int port = -1;
};
TEST_F(HttpServerProtectedTest, ProtectedToken_NoCredentials) {
auto s = new TokenServer();
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
EXPECT_TRUE(rep.www_authenticate.contains("Bearer"));
}
TEST_F(HttpServerProtectedTest, ProtectedToken_Valid) {
auto s = new TokenServer();
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("ok"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Protected_OpenPath) {
auto s = new TokenServer();
s->registerPath("/open", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("open"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/open"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("open"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Protected_CustomDenied) {
auto s = new ForbiddenServer();
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Forbidden, rep.code);
EXPECT_EQ("no"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Basic_NoCredentials) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
EXPECT_TRUE(rep.www_authenticate.contains("realm=\"Restricted\""));
}
TEST_F(HttpServerProtectedTest, Basic_Realm) {
auto s = new PIHTTPServerBasicAuth();
s->setRealm("MyAPI");
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
EXPECT_TRUE(rep.www_authenticate.contains("realm=\"MyAPI\""));
}
TEST_F(HttpServerProtectedTest, Basic_WrongCreds) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo(); });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Basic_Valid) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString & user, const PIString & pass) {
return user == "user" && pass == "pass" ? (PIHTTP::AuthInfo{true, 1}) : PIHTTP::AuthInfo();
});
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("ok"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Basic_LowercaseScheme) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString & user, const PIString & pass) {
return user == "user" && pass == "pass" ? (PIHTTP::AuthInfo{true, 1}) : PIHTTP::AuthInfo();
});
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
// The auth-scheme is case-insensitive (RFC 7235), so "basic" must be accepted.
PIString header = "basic "_a + PIString::fromAscii(PIByteArray::fromAscii("user:pass").toBase64());
auto rep = get(url("/secret"), header);
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("ok"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Basic_BearerHeader) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer x");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Basic_NoCallback) {
auto s = new PIHTTPServerBasicAuth();
EXPECT_FALSE(s->hasBasicAuthCallback());
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Basic_OpenPath) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerPath("/open", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("open"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/open"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("open"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Basic_EmptyPass) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString & user, const PIString & pass) {
return user == "user" && pass == "" ? (PIHTTP::AuthInfo{true, 1}) : PIHTTP::AuthInfo();
});
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), basicAuth("user:"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("ok"_a, rep.body);
}
//! \~english Handler object used to test the object-method protected route overload.
//! \~russian Объект-обработчик для теста перегрузки защищенного маршрута с методом объекта.
class WhoamiHandler {
public:
//! \~english Replies the user id from the authentication result.
//! \~russian Отвечает идентификатором пользователя из результата аутентификации.
PIHTTP::MessageMutable whoami(const PIHTTP::MessageConst &, const PIHTTP::AuthInfo & info) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIString::fromNumber(info.user_id).toByteArray());
}
};
TEST_F(HttpServerProtectedTest, Basic_UserParam) {
auto s = new PIHTTPServerBasicAuth();
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/whoami", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &, const PIHTTP::AuthInfo & info) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIString::fromNumber(info.user_id).toByteArray());
});
ASSERT_TRUE(start(s));
auto rep = get(url("/whoami"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("0"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Basic_MultiUser) {
auto s = new PIHTTPServerBasicAuth();
// Simulates the application-side user table: login and pass -> user id.
s->setBasicAuthCallback([](const PIString & user, const PIString & pass) {
if (user == "alice" && pass == "pass1") return PIHTTP::AuthInfo{true, 1};
if (user == "bob" && pass == "pass2") return PIHTTP::AuthInfo{true, 2};
return PIHTTP::AuthInfo();
});
s->registerProtectedPath("/whoami", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &, const PIHTTP::AuthInfo & info) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIString::fromNumber(info.user_id).toByteArray());
});
ASSERT_TRUE(start(s));
auto alice = get(url("/whoami"), basicAuth("alice:pass1"));
EXPECT_TRUE(alice.finished);
EXPECT_FALSE(alice.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, alice.code);
EXPECT_EQ("1"_a, alice.body);
auto bob = get(url("/whoami"), basicAuth("bob:pass2"));
EXPECT_TRUE(bob.finished);
EXPECT_FALSE(bob.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, bob.code);
EXPECT_EQ("2"_a, bob.body);
auto stranger = get(url("/whoami"), basicAuth("eve:pass3"));
EXPECT_TRUE(stranger.finished);
EXPECT_FALSE(stranger.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, stranger.code);
}
TEST_F(HttpServerProtectedTest, Basic_UserMethod) {
auto s = new PIHTTPServerBasicAuth();
WhoamiHandler handler;
s->setBasicAuthCallback([](const PIString &, const PIString &) { return PIHTTP::AuthInfo{true, 7}; });
ASSERT_TRUE(s->registerProtectedPath("/whoami", PIHTTP::Method::Get, &handler, &WhoamiHandler::whoami));
ASSERT_TRUE(start(s));
auto rep = get(url("/whoami"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("7"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Bearer_NoCredentials) {
auto s = new PIHTTPServerBearerAuth();
s->setBearerAuthCallback([](const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
EXPECT_TRUE(rep.www_authenticate.contains("Bearer realm=\"Restricted\""));
}
TEST_F(HttpServerProtectedTest, Bearer_Realm) {
auto s = new PIHTTPServerBearerAuth();
s->setRealm("MyAPI");
s->setBearerAuthCallback([](const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
EXPECT_TRUE(rep.www_authenticate.contains("Bearer realm=\"MyAPI\""));
}
TEST_F(HttpServerProtectedTest, Bearer_WrongToken) {
auto s = new PIHTTPServerBearerAuth();
s->setBearerAuthCallback([](const PIString &) { return PIHTTP::AuthInfo(); });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Bearer_Valid) {
auto s = new PIHTTPServerBearerAuth();
s->setBearerAuthCallback([](const PIString & token) { return token == "secret" ? (PIHTTP::AuthInfo{true, 1}) : PIHTTP::AuthInfo(); });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("ok"_a, rep.body);
}
TEST_F(HttpServerProtectedTest, Bearer_BasicHeader) {
auto s = new PIHTTPServerBearerAuth();
s->setBearerAuthCallback([](const PIString &) { return PIHTTP::AuthInfo{true}; });
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), basicAuth("user:pass"));
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Bearer_NoCallback) {
auto s = new PIHTTPServerBearerAuth();
EXPECT_FALSE(s->hasBearerAuthCallback());
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
});
ASSERT_TRUE(start(s));
auto rep = get(url("/secret"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code);
}
TEST_F(HttpServerProtectedTest, Bearer_UserInfo) {
auto s = new PIHTTPServerBearerAuth();
WhoamiHandler handler;
s->setBearerAuthCallback([](const PIString & token) { return token == "secret" ? (PIHTTP::AuthInfo{true, 7}) : PIHTTP::AuthInfo(); });
ASSERT_TRUE(s->registerProtectedPath("/whoami", PIHTTP::Method::Get, &handler, &WhoamiHandler::whoami));
ASSERT_TRUE(start(s));
auto rep = get(url("/whoami"), "Bearer secret");
EXPECT_TRUE(rep.finished);
EXPECT_FALSE(rep.transport_error);
EXPECT_EQ(PIHTTP::Code::Ok, rep.code);
EXPECT_EQ("7"_a, rep.body);
}