Compare commits

..

10 Commits

Author SHA1 Message Date
Veloman Yunkan
47f1a61443 Made kiwix-serve friendly to IPv4-only systems
kiwix-serve without an explicitly supplied -i option can henceforth run
on IPv4-only systems. Before this fix, an "Unable to instantiate the
HTTP daemon." error was being reported.
2026-02-23 20:04:04 +04:00
Veloman Yunkan
a3b246fd6d Extracted InternalServer::startMHD(flags, sockaddr) 2026-02-23 19:50:38 +04:00
Veloman Yunkan
75ff043476 Moved some code into InSockAddr::setAnyAddress() 2026-02-23 19:22:03 +04:00
Veloman Yunkan
0f7458c693 Extracted getMHDFlags() 2026-02-23 19:11:43 +04:00
Veloman Yunkan
add7c4cc3a Moved some code into InSockAddr::setAddress() 2026-02-23 19:11:43 +04:00
Veloman Yunkan
fcdc3a7eaf Extracted InSockAddr 2026-02-23 19:09:22 +04:00
Veloman Yunkan
db3705dd23 Errors in InternalServer::startMHD() as exceptions 2026-02-23 18:58:55 +04:00
Veloman Yunkan
3b34130cfc Extracted InternalServer::startMHD() 2026-02-23 18:14:09 +04:00
Veloman Yunkan
b773e93a95 Negative unit tests for starting the server 2026-02-23 18:14:09 +04:00
Veloman Yunkan
e481164258 Extracted test/testing_tools.h 2026-02-23 18:09:33 +04:00
5 changed files with 258 additions and 82 deletions

View File

@@ -85,6 +85,10 @@ namespace kiwix {
namespace
{
void error(const std::string& msg) {
throw std::runtime_error(msg);
}
bool ipAvailable(const std::string addr)
{
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
@@ -99,6 +103,84 @@ bool ipAvailable(const std::string addr)
return false;
}
class InSockAddr
{
private: // data
struct sockaddr_in v4 = {0};
struct sockaddr_in6 v6 = {0};
public: // functions
explicit InSockAddr(int port)
{
v4.sin_family = AF_INET;
v4.sin_port = htons(port);
v6.sin6_family = AF_INET6;
v6.sin6_port = htons(port);
}
IpAddress setAnyAddress(IpMode ipMode)
{
v6.sin6_addr = in6addr_any;
v4.sin_addr.s_addr = htonl(INADDR_ANY);
IpAddress a = kiwix::getBestPublicIps();
if (ipMode == IpMode::IPV6) {
a.addr = "";
} else if (ipMode == IpMode::IPV4) {
a.addr6 = "";
}
return a;
}
IpMode setAddress(const IpAddress& a)
{
const std::string addrStr = !a.addr.empty() ? a.addr : a.addr6;
const int r1 = inet_pton(AF_INET, a.addr.c_str(), &v4.sin_addr.s_addr);
const int r2 = inet_pton(AF_INET6, a.addr6.c_str(), &v6.sin6_addr.s6_addr);
if ( r1 != 1 && r2 != 1 ) {
error("invalid IP address: " + addrStr);
}
if ( !ipAvailable(addrStr) ) {
error("IP address is not available on this system: " + addrStr);
}
return !a.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
}
struct sockaddr* sockaddr(IpMode ipMode) const
{
return (ipMode==IpMode::ALL || ipMode==IpMode::IPV6)
? (struct sockaddr*)&v6
: (struct sockaddr*)&v4;
}
};
int getMHDFlags(IpMode ipMode, bool verbose)
{
#ifdef _WIN32
int flags = MHD_USE_SELECT_INTERNALLY;
#else
int flags = MHD_USE_POLL_INTERNALLY;
#endif
if (ipMode == IpMode::ALL) {
flags |= MHD_USE_DUAL_STACK;
} else if (ipMode == IpMode::IPV6) {
flags |= MHD_USE_IPv6;
}
if (verbose) {
flags |= MHD_USE_DEBUG;
}
return flags;
}
std::string
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
{
@@ -450,81 +532,61 @@ InternalServer::InternalServer(LibraryPtr library,
InternalServer::~InternalServer() = default;
bool InternalServer::start() {
#ifdef _WIN32
int flags = MHD_USE_SELECT_INTERNALLY;
#else
int flags = MHD_USE_POLL_INTERNALLY;
#endif
if (m_verbose.load())
flags |= MHD_USE_DEBUG;
struct sockaddr_in sockAddr4={0};
sockAddr4.sin_family = AF_INET;
sockAddr4.sin_port = htons(m_port);
struct sockaddr_in6 sockAddr6={0};
sockAddr6.sin6_family = AF_INET6;
sockAddr6.sin6_port = htons(m_port);
struct MHD_Daemon* InternalServer::startMHD(int flags,
struct sockaddr* sockaddr)
{
return MHD_start_daemon(flags,
m_port,
NULL,
NULL,
&staticHandlerCallback,
this,
MHD_OPTION_SOCK_ADDR, sockaddr,
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
MHD_OPTION_END);
}
void InternalServer::startMHD() {
InSockAddr inSockAddr(m_port);
if (m_addr.addr.empty() && m_addr.addr6.empty()) { // No ip address provided
if (m_ipMode == IpMode::AUTO) m_ipMode = IpMode::ALL;
sockAddr6.sin6_addr = in6addr_any;
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
IpAddress bestIps = kiwix::getBestPublicIps();
if (m_ipMode == IpMode::IPV4 || m_ipMode == IpMode::ALL) m_addr.addr = bestIps.addr;
if (m_ipMode == IpMode::IPV6 || m_ipMode == IpMode::ALL) m_addr.addr6 = bestIps.addr6;
m_addr = inSockAddr.setAnyAddress(m_ipMode);
} else {
const std::string addr = !m_addr.addr.empty() ? m_addr.addr : m_addr.addr6;
if (m_ipMode != kiwix::IpMode::AUTO) {
std::cerr << "ERROR: When an IP address is provided the IP mode must not be set" << std::endl;
return false;
error("When an IP address is provided the IP mode must not be set");
}
bool validV4 = inet_pton(AF_INET, m_addr.addr.c_str(), &(sockAddr4.sin_addr.s_addr)) == 1;
bool validV6 = inet_pton(AF_INET6, m_addr.addr6.c_str(), &(sockAddr6.sin6_addr.s6_addr)) == 1;
if (!validV4 && !validV6) {
std::cerr << "ERROR: invalid IP address: " << addr << std::endl;
return false;
}
if (!ipAvailable(addr)) {
std::cerr << "ERROR: IP address is not available on this system: " << addr << std::endl;
return false;
}
m_ipMode = !m_addr.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
m_ipMode = inSockAddr.setAddress(m_addr);
}
if (m_ipMode == IpMode::ALL) {
flags|=MHD_USE_DUAL_STACK;
} else if (m_ipMode == IpMode::IPV6) {
flags|=MHD_USE_IPv6;
const int flags = getMHDFlags(m_ipMode, m_verbose.load());
mp_daemon = startMHD(flags, inSockAddr.sockaddr(m_ipMode));
if (mp_daemon == nullptr && m_ipMode == IpMode::ALL) {
// MHD_USE_DUAL_STACK (set in IpMode::ALL case) fails on systems with IPv6
// disabled. Let's retry in IPv4-only mode.
m_ipMode = IpMode::IPV4;
m_addr.addr6 = "";
const int flags = getMHDFlags(m_ipMode, m_verbose.load());
mp_daemon = startMHD(flags, inSockAddr.sockaddr(m_ipMode));
}
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
? (struct sockaddr*)&sockAddr6
: (struct sockaddr*)&sockAddr4;
mp_daemon = MHD_start_daemon(flags,
m_port,
NULL,
NULL,
&staticHandlerCallback,
this,
MHD_OPTION_SOCK_ADDR, sockaddr,
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
MHD_OPTION_END);
if (mp_daemon == nullptr) {
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
<< " is maybe already occupied or need more permissions to be open. "
"Please try as root or with a port number higher or equal to 1024."
<< std::endl;
error("Unable to instantiate the HTTP daemon. "
"The port " + kiwix::to_string(m_port) + " is maybe already occupied"
" or need more permissions to be open. "
"Please try as root or with a port number higher or equal to 1024.");
}
}
bool InternalServer::start() {
try {
startMHD();
} catch (const std::runtime_error& err ) {
std::cerr << "ERROR: " << err.what() << std::endl;
return false;
}
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
m_server_id = kiwix::to_string(server_start_time.count());
return true;

View File

@@ -126,6 +126,8 @@ class InternalServer {
IpMode getIpMode() const { return m_ipMode; }
private: // functions
void startMHD();
struct MHD_Daemon* startMHD(int flags, struct sockaddr* sockaddr);
std::unique_ptr<Response> handle_request(const RequestContext& request);
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
std::unique_ptr<Response> build_homepage(const RequestContext& request);

View File

@@ -2,6 +2,9 @@
#include "../include/library.h"
#include "../include/manager.h"
#include "testing_tools.h"
using namespace kiwix::testing;
#include "gtest/gtest.h"
namespace
@@ -58,27 +61,6 @@ class NameMapperTest : public ::testing::Test {
std::shared_ptr<kiwix::Library> lib;
};
class CapturedStderr
{
std::ostringstream buffer;
std::streambuf* const sbuf;
public:
CapturedStderr()
: sbuf(std::cerr.rdbuf())
{
std::cerr.rdbuf(buffer.rdbuf());
}
CapturedStderr(const CapturedStderr&) = delete;
~CapturedStderr()
{
std::cerr.rdbuf(sbuf);
}
operator std::string() const { return buffer.str(); }
};
#if _WIN32
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
"Path collision: 'C:\\data\\zero_four_2021-10.zim' and"

View File

@@ -8,6 +8,8 @@
#include "../src/tools/stringTools.h"
#include "testing_tools.h"
using namespace kiwix::testing;
const std::string ROOT_PREFIX("/ROOT%23%3F");
@@ -2306,3 +2308,74 @@ TEST_F(ServerTest, EmptyPatternSearchDoesNotError)
{
EXPECT_EQ(200, zfs1_->GET("/ROOT%23%3F/search?content=zimfile")->status);
}
#define EXPECT_ERROR(MSG, SERVER_SETUP_CODE) \
{ \
kiwix::Server server(kiwix::Library::create()); \
CapturedStderr stderror; \
SERVER_SETUP_CODE; \
EXPECT_FALSE(server.start()); \
EXPECT_EQ(std::string(stderror), std::string("ERROR: ") + MSG + "\n"); \
}
TEST(ServerNegativeTest, IpAddressAndIpModeAreMutuallyExclusive)
{
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
server.setAddress("127.0.0.1");
server.setIpMode(kiwix::IpMode::IPV4);
);
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
server.setAddress("[::1]");
server.setIpMode(kiwix::IpMode::IPV6);
);
EXPECT_ERROR("When an IP address is provided the IP mode must not be set",
server.setAddress("localhost");
server.setIpMode(kiwix::IpMode::ALL);
);
}
TEST(ServerNegativeTest, InvalidIpAddressDetection)
{
EXPECT_ERROR("invalid IP address: 1.2.3",
server.setAddress("1.2.3");
);
EXPECT_ERROR("invalid IP address: 127.0.0.256",
server.setAddress("127.0.0.256");
);
EXPECT_ERROR("invalid IP address: localhost",
server.setAddress("localhost");
);
EXPECT_ERROR("invalid IP address: fe80::94d2:16e7:5f3e:89bx",
server.setAddress("[fe80::94d2:16e7:5f3e:89bx]");
);
// We assume that our unit tests won't be run on Google's DNS server
EXPECT_ERROR("IP address is not available on this system: 8.8.8.8",
server.setAddress("8.8.8.8");
);
// According to the spec, IPv6 addresses 2001:db8::/32 are reserved
// for documentation and example source code
EXPECT_ERROR("IP address is not available on this system: 2001:db8::",
server.setAddress("[2001:db8::]");
);
}
TEST(ServerNegativeTest, UnusablePort)
{
// Occupy port 8910
httplib::Server portOccupant;
ASSERT_TRUE(portOccupant.bind_to_port("127.0.0.1", 8910));
// Try to listen on the same port
EXPECT_ERROR("Unable to instantiate the HTTP daemon. The port 8910 is maybe "
"already occupied or need more permissions to be open. Please "
"try as root or with a port number higher or equal to 1024.",
server.setPort(8910);
);
}

57
test/testing_tools.h Normal file
View File

@@ -0,0 +1,57 @@
/*
* Copyright 2026 Veloman Yunkan
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef LIBKIWIX_TESTING_TOOLS_H
#define LIBKIWIX_TESTING_TOOLS_H
#include <sstream>
#include <iostream>
namespace kiwix
{
namespace testing
{
class CapturedStderr
{
std::ostringstream buffer;
std::streambuf* const sbuf;
public:
CapturedStderr()
: sbuf(std::cerr.rdbuf())
{
std::cerr.rdbuf(buffer.rdbuf());
}
CapturedStderr(const CapturedStderr&) = delete;
~CapturedStderr()
{
std::cerr.rdbuf(sbuf);
}
operator std::string() const { return buffer.str(); }
};
} // namespace testing
} // namespace kiwix
#endif // LIBKIWIX_TESTING_TOOLS_H