#include "probe.h"
#include <array>
#include <cerrno>
#include <cstring>
#include <future>
#include <stop_token>
#include <thread>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <gtest/gtest.h>
namespace
{
using namespace std::chrono_literals;
bool waitReadable(int fd, std::stop_token stop)
{
while(!stop.stop_requested())
{
pollfd descriptor{fd, POLLIN, 0};
const int ready = poll(&descriptor, 1, 20);
if(ready > 0)
{
return true;
}
if(ready < 0 && errno != EINTR)
{
return false;
}
}
return false;
}
class ProbeTest : public testing::Test
{
protected:
void TearDown() override
{
worker.request_stop();
if(worker.joinable())
{
worker.join();
}
if(server >= 0)
{
close(server);
}
}
bool bindServer(int type, int family = AF_INET, bool listening = false)
{
server = socket(family, type | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if(server < 0)
{
return false;
}
sockaddr_storage address{};
socklen_t size = 0;
if(family == AF_INET)
{
auto& ipv4 = reinterpret_cast<sockaddr_in&>(address);
ipv4.sin_family = AF_INET;
ipv4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
size = sizeof(ipv4);
}
else
{
auto& ipv6 = reinterpret_cast<sockaddr_in6&>(address);
ipv6.sin6_family = AF_INET6;
ipv6.sin6_addr = in6addr_loopback;
size = sizeof(ipv6);
}
if(bind(server, reinterpret_cast<sockaddr*>(&address), size) < 0 ||
getsockname(server, reinterpret_cast<sockaddr*>(&address),
&size) < 0)
{
return false;
}
port = family == AF_INET
? ntohs(reinterpret_cast<sockaddr_in&>(address).sin_port)
: ntohs(reinterpret_cast<sockaddr_in6&>(address).sin6_port);
return !listening || listen(server, 8) == 0;
}
static void serveHttp(std::stop_token stop, ProbeTest* self,
int status, bool silent)
{
if(!waitReadable(self->server, stop))
{
return;
}
const int client = accept4(self->server, nullptr, nullptr,
SOCK_NONBLOCK | SOCK_CLOEXEC);
if(client < 0)
{
return;
}
std::string request;
while(request.find("\r\n\r\n") == std::string::npos &&
request.size() < 8192 && waitReadable(client, stop))
{
std::array<char, 1024> buffer;
const auto count = recv(client, buffer.data(), buffer.size(), 0);
if(count <= 0)
{
break;
}
request.append(buffer.data(), count);
}
if(!silent)
{
const auto response = "HTTP/1.1 " + std::to_string(status) +
" Test\r\nContent-Length: 2\r\nConnection: close\r\n"
"Location: /redirect\r\n\r\nok";
send(client, response.data(), response.size(), MSG_NOSIGNAL);
}
else
{
// Wait for the probe to time out and close its connection.
while(waitReadable(client, stop))
{
char buffer;
if(recv(client, &buffer, 1, 0) <= 0)
{
break;
}
}
}
close(client);
}
static void serveUdp(std::stop_token stop, ProbeTest* self,
bool empty_reply)
{
if(!waitReadable(self->server, stop))
{
return;
}
std::array<char, 1024> buffer;
sockaddr_storage source{};
socklen_t size = sizeof(source);
const auto count = recvfrom(self->server, buffer.data(), buffer.size(),
0, reinterpret_cast<sockaddr*>(&source), &size);
if(count >= 0)
{
self->received_payload.assign(buffer.data(), count);
const std::string reply = empty_reply ? "" : "reply";
sendto(self->server, reply.data(), reply.size(), MSG_NOSIGNAL,
reinterpret_cast<sockaddr*>(&source), size);
}
}
int server = -1;
std::uint16_t port = 0;
std::string received_payload;
std::jthread worker;
};
TEST(Probe, InvalidConfiguration)
{
EXPECT_FALSE(createProbe(TcpEndpoint{"", 80}));
EXPECT_FALSE(createProbe(TcpEndpoint{"localhost", 0}));
EXPECT_FALSE(createProbe(UdpEndpoint{"localhost", 0, ""}));
EXPECT_FALSE(createProbe(UdpEndpoint{"localhost", 80,
std::string(65508, 'x')}));
EXPECT_FALSE(createProbe(IcmpEndpoint{""}));
EXPECT_FALSE(createProbe(HttpEndpoint{"file:///etc/hosts"}));
EXPECT_FALSE(createProbe(HttpEndpoint{"http://"}));
EXPECT_FALSE(createProbe(HttpEndpoint{"http://localhost"}, 0s));
EXPECT_FALSE(createProbe(TcpEndpoint{"localhost", 80}, -1s));
EXPECT_FALSE(createProbe(TcpEndpoint{
"localhost", 80}, std::chrono::seconds::max()));
EXPECT_FALSE(createProbe(UdpEndpoint{"localhost", 80, ""}, 0s));
EXPECT_FALSE(createProbe(IcmpEndpoint{"localhost"}, 0s));
EXPECT_FALSE(createProbe(TcpEndpoint{
std::string("host\0suffix", 11), 80}));
}
TEST(Probe, FactoryCreatesConcreteTypes)
{
auto http = createProbe(HttpEndpoint{"https://example.invalid"});
ASSERT_TRUE(http);
EXPECT_NE(dynamic_cast<HttpProbe*>(http->get()), nullptr);
auto tcp = createProbe(TcpEndpoint{"localhost", 80});
ASSERT_TRUE(tcp);
EXPECT_NE(dynamic_cast<TcpProbe*>(tcp->get()), nullptr);
auto udp = createProbe(UdpEndpoint{"localhost", 53, ""});
ASSERT_TRUE(udp);
EXPECT_NE(dynamic_cast<UdpProbe*>(udp->get()), nullptr);
auto icmp = createProbe(IcmpEndpoint{"localhost"});
ASSERT_TRUE(icmp);
EXPECT_NE(dynamic_cast<IcmpProbe*>(icmp->get()), nullptr);
}
TEST_F(ProbeTest, OwnsConfiguration)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
mw::E<std::unique_ptr<ProbeInterface>> probe;
{
TcpEndpoint config{"127.0.0.1", port};
probe = createProbe(config, 1s);
ASSERT_TRUE(probe) << probe.error().msg();
config.host.clear();
config.port = 0;
}
auto result = (*probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
mw::E<ProbeResult> runOwnedProbe(std::unique_ptr<ProbeInterface> probe)
{
return probe->probe();
}
TEST_F(ProbeTest, TaskOwnsProbe)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
auto probe = createProbe(TcpEndpoint{"127.0.0.1", port}, 1s);
ASSERT_TRUE(probe) << probe.error().msg();
auto task = std::async(std::launch::async, runOwnedProbe,
std::move(*probe));
EXPECT_EQ(probe->get(), nullptr);
auto result = task.get();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
TEST_F(ProbeTest, TcpConnectAndTiming)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
const auto before = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
auto result_probe = createProbe(TcpEndpoint{"127.0.0.1", port}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
EXPECT_GE(result->timestamp, before);
EXPECT_GE(result->duration_microsecond, 0);
}
TEST_F(ProbeTest, TcpIpv6)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET6, true));
auto result_probe = createProbe(TcpEndpoint{"::1", port}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
TEST_F(ProbeTest, HostnameResolution)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
auto result_probe = createProbe(TcpEndpoint{"localhost", port}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
TEST_F(ProbeTest, RefusedTcpAndHttp)
{
ASSERT_TRUE(bindServer(SOCK_STREAM));
auto tcp_probe = createProbe(TcpEndpoint{"127.0.0.1", port}, 1s);
ASSERT_TRUE(tcp_probe) << tcp_probe.error().msg();
auto tcp = (*tcp_probe)->probe();
ASSERT_TRUE(tcp) << tcp.error().msg();
EXPECT_EQ(tcp->status, ProbeStatus::BAD);
auto http_probe = createProbe(HttpEndpoint{
"http://127.0.0.1:" + std::to_string(port)}, 1s);
ASSERT_TRUE(http_probe) << http_probe.error().msg();
auto http = (*http_probe)->probe();
ASSERT_TRUE(http) << http.error().msg();
EXPECT_EQ(http->status, ProbeStatus::BAD);
}
TEST_F(ProbeTest, HttpStatusCodes)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
for(int status : {200, 204, 299, 301, 404, 503})
{
worker = std::jthread(&ProbeTest::serveHttp, this, status, false);
auto result_probe = createProbe(HttpEndpoint{
"http://127.0.0.1:" + std::to_string(port)}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, status >= 200 && status < 300
? ProbeStatus::GOOD : ProbeStatus::BAD) << status;
worker.join();
}
}
TEST_F(ProbeTest, HttpTimeout)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
worker = std::jthread(&ProbeTest::serveHttp, this, 200, true);
auto result_probe = createProbe(HttpEndpoint{
"http://127.0.0.1:" + std::to_string(port)}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::BAD);
EXPECT_GE(result->duration_microsecond, 900000);
EXPECT_LT(result->duration_microsecond, 3000000);
}
TEST_F(ProbeTest, UdpReplyAndBinaryPayload)
{
ASSERT_TRUE(bindServer(SOCK_DGRAM));
worker = std::jthread(&ProbeTest::serveUdp, this, false);
const std::string payload("hello\0world", 11);
auto result_probe = createProbe(
UdpEndpoint{"127.0.0.1", port, payload}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
worker.join();
EXPECT_EQ(received_payload, payload);
}
TEST_F(ProbeTest, UdpEmptyReplyAndIpv6)
{
ASSERT_TRUE(bindServer(SOCK_DGRAM, AF_INET6));
worker = std::jthread(&ProbeTest::serveUdp, this, true);
auto result_probe = createProbe(UdpEndpoint{"::1", port, ""}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
worker.join();
EXPECT_TRUE(received_payload.empty());
}
TEST_F(ProbeTest, UdpSilenceIsOther)
{
ASSERT_TRUE(bindServer(SOCK_DGRAM));
auto result_probe = createProbe(
UdpEndpoint{"127.0.0.1", port, "probe"}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::OTHER);
EXPECT_GE(result->duration_microsecond, 900000);
EXPECT_LT(result->duration_microsecond, 3000000);
}
TEST_F(ProbeTest, UdpRefused)
{
ASSERT_TRUE(bindServer(SOCK_DGRAM));
close(server);
server = -1;
auto result_probe = createProbe(
UdpEndpoint{"127.0.0.1", port, "probe"}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::BAD);
}
TEST(Probe, DnsFailureIsBad)
{
auto result_probe = createProbe(
TcpEndpoint{"status-tracker.invalid", 80}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::BAD);
EXPECT_LT(result->duration_microsecond, 3000000);
}
TEST(Probe, IcmpIpv4)
{
const int allowed = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
auto result_probe = createProbe(IcmpEndpoint{"127.0.0.1"}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
if(allowed < 0)
{
EXPECT_FALSE(result);
GTEST_SKIP() << "ICMP ping sockets unavailable for this process";
}
close(allowed);
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
TEST(Probe, IcmpIpv6)
{
const int allowed = socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
auto result_probe = createProbe(IcmpEndpoint{"::1"}, 1s);
ASSERT_TRUE(result_probe) << result_probe.error().msg();
auto result = (*result_probe)->probe();
if(allowed < 0)
{
EXPECT_FALSE(result);
GTEST_SKIP() << "ICMPv6 ping sockets unavailable for this process";
}
close(allowed);
ASSERT_TRUE(result) << result.error().msg();
EXPECT_EQ(result->status, ProbeStatus::GOOD);
}
}