Files
pip/tests/http_server/http_server_tls_test.cpp

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