276 lines
12 KiB
C++
276 lines
12 KiB
C++
//! \~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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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 <cstring>
|
|
|
|
|
|
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);
|
|
}
|