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