//! \~english Tests for the HTTPS/TLS mode of the HTTP server: a self-signed certificate is passed //! through the in-memory MicrohttpdServer options and the session auth flow is exercised with //! PIHTTPClient over TLS. //! \~russian Тесты режима HTTPS/TLS HTTP-сервера: самоподписанный сертификат передается через //! in-memory опции MicrohttpdServer, а сессионная аутентификация проверяется клиентом PIHTTPClient //! поверх TLS. /* PIP - Platform Independent Primitives This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #include "pihttpclient.h" #include "pihttpservermodule.h" #include "pijson.h" #include "piliterals_string.h" #include "piliterals_time.h" #include "pimutex.h" #include "pisemaphore.h" #include "pistring.h" #include "pivariant.h" #include "gtest/gtest.h" #include namespace { //! \~english Throwaway self-signed key pair used only by this test. \c CN=localhost with //! \c subjectAltName = DNS:localhost, IP:127.0.0.1; validity is 100 years so the test does not //! depend on the wall clock. Do not use this key anywhere else. //! \~russian Одноразовая самоподписанная пара ключей только для этого теста. \c CN=localhost, //! \c subjectAltName = DNS:localhost, IP:127.0.0.1; срок действия 100 лет, чтобы тест не зависел //! от текущей даты. Нигде больше этот ключ не использовать. const char kTestCert[] = R"PEM(-----BEGIN CERTIFICATE----- MIIDJzCCAg+gAwIBAgIUN3x31qNyOdse7OKWevqLxeSb1CgwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDkyMzEwMjkyNloYDzIxMjYw ODMwMTAyOTI2WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQCzyG9JqN8Sh5KgXW8QqRsEDV7hJXJxP/IULbgCEpTr X2zaGvHPk5FextafQpsbAbMocZ8Cqv+Isi4gBrRURmOs/07fJLMoULtiaF3rrVk1 OrrJikiEpNdn25ienBPB/R95gNwB0Oez66b2FzJtimj3Y8vHeGCM9dWqhenPGNYO aT3T3k/y2UND1g7v3PHBCNzorrBz7stiFmKBcAr5YeCz7ulTNiN3kT4OlAv8ozhW LX1mztXulVrcTm/e62hcUHj7kKYHzDkBGK7V3vGZvTZm+yUYFkyWacsyzCnf4QhG uMj8iAjyh5LbfhuvWBVo4+UEqxSgbuzEE1w/3AQe4OHrAgMBAAGjbzBtMB0GA1Ud DgQWBBSMcO74Pt1uumFbY2bD1nlvkpBg0TAfBgNVHSMEGDAWgBSMcO74Pt1uumFb Y2bD1nlvkpBg0TAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEADJ2wOYVJ2/uruug2rstDYBYApYX3 o4sLFW+d8M1Zq+9wEcDYRgGszbAGE5qvkg2cKw4wGKB0zXRGvQIIDzcxawFbB/0q ImJ24KkbD9I1f2vdmF2OHj9L+pCgxVg2nOqzkenpRci99sEq5FEufQCJas6Okks9 0tenWxZ6ShD8TjWIBH455ePLXbw+WJhESgDOealrLSZRoYjTv/GswnYALb9e6V7q jEmrJ/RHj7zrUWhewBTvqwErEOPks7fr96oR/2emvgXKHftBIzPSHps4/W6fpV7h p0J4VdPuuQ/SCxPgE7+hUvn93w96ukEj6bz4EWDitQnqnfWTfuCDclxsfg== -----END CERTIFICATE----- )PEM"; const char kTestKey[] = R"PEM(-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzyG9JqN8Sh5Kg XW8QqRsEDV7hJXJxP/IULbgCEpTrX2zaGvHPk5FextafQpsbAbMocZ8Cqv+Isi4g BrRURmOs/07fJLMoULtiaF3rrVk1OrrJikiEpNdn25ienBPB/R95gNwB0Oez66b2 FzJtimj3Y8vHeGCM9dWqhenPGNYOaT3T3k/y2UND1g7v3PHBCNzorrBz7stiFmKB cAr5YeCz7ulTNiN3kT4OlAv8ozhWLX1mztXulVrcTm/e62hcUHj7kKYHzDkBGK7V 3vGZvTZm+yUYFkyWacsyzCnf4QhGuMj8iAjyh5LbfhuvWBVo4+UEqxSgbuzEE1w/ 3AQe4OHrAgMBAAECggEAEe0HuhN+TDQVmN6k5vqSXzp23gIq20OIZesTfUY3Cd6i iH8sfAl3WQWzrrH1RaF+W9qaA6njMtES+LSFoVJanyXelzDrADd67g2aZJgM3HOp RKEzwkeuOesGchvKSIrU0ZU/2pqAdksmUBFXp0TbjhdDevWNj8b1vm8xAfca/z99 iokKNNmEIofEVO/yfDqbCI2Gn51aI9rucQ4RzTT3fX68tfcn5i1UChcBZdSCCTxV zaR0H2V//4UKFnv06mLHShszhlDX+Q1P7FTLTDvBqEafaSInuVTQBolCG/3AAkE5 DmPwsqKoVST8NdtX5S1G1eeIGCAnmlt973eFz9ZcYQKBgQDXjsmAIhu+OD5VmVQ9 LYFekb0yoR68zS7piAV+D/ZblA9qHpzZPpzzZyP4YDovVNE+uIJYe8tv9WdmfWzT av0cI8iQ2YYkxTQrpnCRhk71KQbv3mlyGmCLw7mMKMdnFK87WlPjg8T8h+QGIUlW fdCm8tTydi3S5bTjxX+36YASsQKBgQDVg2M+nBM0y5Ev3XYOgoP1F37O8Z0pQqGy OAnqoDWAD38RlDWVk41CBNj0kwUCGs1qSOKnP098FnX1YWoTuf9bvh4gu61X/Tkm QAWD9cm2NnHVmSQMYDM5BNLr/LKwy5EvL/lm86GmM1z6p6w4RfD2gOjmGxtelD6B 07YIBStNWwKBgClbbfG1mZkPdXY66tyqBG4+jAxQuXMD8wI/ZL5hFaVQbPyxTuqn hrl0ioQ3AjLelR7xi6Cqb8fVT7dsTRrrJhDmFK8l7QTVCUNutZ5WJIQJERmolJad 1PQ74zevA0MVxuEeXYYDXCIPeIJslIORtByz7oNuhTA3flNNK0n/9OChAoGBAIe8 v5tOHeeygtqrMCKJVtq51SQ69m8ErUdf2O4Qa/K950qFrospAuS1sDfMP6LW50Bq la090opQvS0CgS2JUY5Bj2W/6rPFiE+I4/jAiTwN4NCqSwCTJ/1X07+zJua+g847 8ZJB0MKYZMP1VMdxr38G9QlUslGubQ6cSV1P6/eBAoGAWotoJIlWZzWamuwVyxrx 340G+9jd9k5Mxr6LbsA2ox0MquDrsrWZ7CIl7MQ4BNT35xIEZnEOQJ+pZSpzdtsF yROGVbWwt/jwkQws9ePQINKuh/K0njRn0sJFwOOFNlW1tB2Xw5oey4Rdc/gijc4s zs7YMkHAqzsDBONCc+0kong= -----END PRIVATE KEY----- )PEM"; //! \~english Session server with a single test user, running in TLS mode. //! \~russian Сервер сессий с одним тестовым пользователем, работающий в режиме TLS. class TlsUserServer: public PIHTTPServerSessionAuth { protected: PIHTTP::AuthInfo checkCredentials(const PIString & login, const PIString & pass) override { if (login == "alice" && pass == "pass1") return PIHTTP::AuthInfo(true, 1); return PIHTTP::AuthInfo(); } }; } // namespace class HttpServerTlsTest: public ::testing::Test { protected: struct Reply { PIHTTP::Code code = PIHTTP::Code::Unknown; PIString body; PIString www_authenticate; bool finished = false; bool transport_error = false; }; //! \~english Performs an HTTPS request to "url" with optional JSON body and "Authorization" //! header. "ignore_ssl" additionally disables host/certificate checks on the client. //! \~russian Выполняет HTTPS-запрос к "url" с необязательным JSON-телом и заголовком //! "Authorization". "ignore_ssl" дополнительно отключает проверки сертификата на клиенте. static Reply request(PIHTTP::Method method, const PIString & url, const PIString & body, const PIString & auth, bool ignore_ssl = false) { // The reply state and semaphore are heap-allocated: 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); if (body.isNotEmpty()) { req.addHeader(PIHTTP::Header::ContentType, "application/json"); req.setBody(body.toByteArray()); } auto client = PIHTTPClient::create(url, method, req); if (ignore_ssl) client->ignoreSSLErrors(); 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([rep, sem](const PIHTTP::MessageConst &) { 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). return Reply(); } static PIString loginJson(const PIString & login, const PIString & password) { PIJSON j; j["login"] = login; j["password"] = password; return j.toJSON(PIJSON::Compact); } static PIString bearer(const PIString & token) { return "Bearer %1"_a.arg(token); } //! \~english Logs in over HTTPS and returns the issued token (empty on failure). //! \~russian Выполняет вход по HTTPS и возвращает выданный токен (пустой при неудаче). static PIString login(const PIString & url, const PIString & login, const PIString & password, bool ignore_ssl = false) { auto rep = request(PIHTTP::Method::Post, url + "/api/login", loginJson(login, password), "", ignore_ssl); if (rep.code != PIHTTP::Code::Ok) return PIString(); return PIJSON::fromJSON(rep.body)["token"].toString(); } //! \~english Starts a TLS server on the first free port from 18562 and keeps it for teardown. //! \~russian Запускает TLS-сервер на первом свободном порту от 18562 и сохраняет его для завершения. bool start(TlsUserServer * s) { server = s; s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &, const PIHTTP::AuthInfo & info) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok) .setBody(("hello user " + PIString::fromNumber(info.user_id)).toByteArray()); }); s->setOption(MicrohttpdServer::Option::HTTPSEnabled, PIVariant(true)); s->setOption(MicrohttpdServer::Option::HTTPSMemCert, PIVariant(PIByteArray(kTestCert, (uint)std::strlen(kTestCert)))); s->setOption(MicrohttpdServer::Option::HTTPSMemKey, PIVariant(PIByteArray(kTestKey, (uint)std::strlen(kTestKey)))); for (port = 18562; port < 18662; ++port) { if (s->listenAll((ushort)port)) return true; } port = -1; return false; } PIString url(const char * path) const { return "https://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(HttpServerTlsTest, LoginAndProtectedOverTls) { auto s = new TlsUserServer(); ASSERT_TRUE(start(s)); // No token -> 401 with a Bearer challenge. auto no_auth = request(PIHTTP::Method::Get, url("/secret"), "", ""); EXPECT_TRUE(no_auth.finished); EXPECT_FALSE(no_auth.transport_error); EXPECT_EQ(PIHTTP::Code::Unauthorized, no_auth.code); EXPECT_TRUE(no_auth.www_authenticate.contains("Bearer")); PIString token = login(url(""), "alice", "pass1"); ASSERT_FALSE(token.isEmpty()); auto sec = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_TRUE(sec.finished); EXPECT_FALSE(sec.transport_error); EXPECT_EQ(PIHTTP::Code::Ok, sec.code); EXPECT_EQ("hello user 1"_a, sec.body); } TEST_F(HttpServerTlsTest, BadCredentialsOverTls) { auto s = new TlsUserServer(); ASSERT_TRUE(start(s)); auto rep = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "wrong"), ""); EXPECT_TRUE(rep.finished); EXPECT_FALSE(rep.transport_error); EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code); EXPECT_TRUE(rep.body.contains("invalid credentials")); } TEST_F(HttpServerTlsTest, IgnoreSslErrorsStillWorks) { auto s = new TlsUserServer(); ASSERT_TRUE(start(s)); PIString token = login(url(""), "alice", "pass1", true); ASSERT_FALSE(token.isEmpty()); auto sec = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token), true); EXPECT_EQ(PIHTTP::Code::Ok, sec.code); EXPECT_EQ("hello user 1"_a, sec.body); } TEST_F(HttpServerTlsTest, LogoutOverTls) { auto s = new TlsUserServer(); ASSERT_TRUE(start(s)); PIString token = login(url(""), "alice", "pass1"); ASSERT_FALSE(token.isEmpty()); EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code); auto out = request(PIHTTP::Method::Post, url("/api/logout"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::NoContent, out.code); auto dead = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Unauthorized, dead.code); }