Compare commits

...

11 Commits

Author SHA1 Message Date
Veloman Yunkan
34fc831c13 Documentation of a subset of Server API
Documentation of libkiwix API has been largely neglected. This commit
documents the Server API having to do with the functionality fixed in
the previous commit.
2026-02-24 16:10:57 +04:00
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
9 changed files with 358 additions and 90 deletions

View File

@@ -18,7 +18,7 @@ import os
# -- Project information -----------------------------------------------------
project = 'libkiwix'
copyright = '2022, libkiwix-team'
copyright = '2026, libkiwix-team'
author = 'libkiwix-team'

View File

@@ -14,12 +14,43 @@
#endif
namespace kiwix {
namespace kiwix
{
/**
* `IpMode` is used to [configure](@ref Server::setIpMode()) a `Server` object
* to listen for incoming connections not on a single IP address but on all
* addresses of the specified family.
*/
enum class IpMode
{
/**
* Listen on all IPv4 addresses.
*/
IPV4,
/**
* Listen on all IPv6 addresses.
*/
IPV6,
/**
* Listen on all available addresses.
*/
ALL,
/**
* `IpMode::AUTO` (which is the default) must be used when an explicit
* (non-empty) IP address for listening is provided via
* `Server::setAddress()`. If no such address is enforced, then
* `IpMode::AUTO` is equivalent to `IpMode::ALL`.
*/
AUTO
};
enum class IpMode { IPV4, IPV6, ALL, AUTO }; // AUTO: Server decides the protocol
typedef zim::size_type size_type;
typedef zim::offset_type offset_type;
}
} // namespace kiwix
#endif //_KIWIX_COMMON_H_

View File

@@ -42,7 +42,7 @@ namespace kiwix
virtual ~Server();
/**
* Serve the content.
* Start serving the content.
*/
bool start();
@@ -51,9 +51,27 @@ namespace kiwix
*/
void stop();
/**
* Set the path of the root URL served by this server instance.
*/
void setRoot(const std::string& root);
/**
* Set the IP address on which to listen for incoming connections.
*
* Specifying a non-empty IP address requires that the IpMode is
* [set](@ref setIpMode()) to `IpMode::AUTO` (which is the default).
* Otherwise, [starting](@ref start()) the server will fail.
*/
void setAddress(const std::string& addr);
/**
* Set the port on which to listen for incoming connections.
*
* Default port is 80, but using it requires special privileges.
*/
void setPort(int port) { m_port = port; }
void setNbThreads(int threads) { m_nbThreads = threads; }
void setMultiZimSearchLimit(unsigned int limit) { m_multizimSearchLimit = limit; }
void setIpConnectionLimit(int limit) { m_ipConnectionLimit = limit; }
@@ -65,10 +83,50 @@ namespace kiwix
{ m_blockExternalLinks = blockExternalLinks; }
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
/**
* Listen for incoming connections on all IP addresses of the specified
* IP protocol family.
*/
void setIpMode(IpMode mode) { m_ipMode = mode; }
/**
* Get the port on which the server listens for incoming connections
*/
int getPort() const;
/**
* Get the IPv4 and/or IPv6 address(es) on which the server can be
* contacted.
*
* The server may actually be listening on other IP addresses as well
* (see `setIpMode()`). The IP address(es) returned by this method
* represent the best-guess public IP address(es) accessible by the
* broadest set of clients.
*/
IpAddress getAddress() const;
/**
* Get the effective IpMode used by this server.
*
* The returned value may be different from the one configured via
* `setIpMode()`, since the server is expected to adjust to the
* constraints of the environment (e.g. `IpMode::ALL` can be converted
* to `IpMode::IPV4` on a system that does not support IPv6).
*/
IpMode getIpMode() const;
/**
* Get the list of HTTP URLs through which the server can be contacted.
*
* Each URL is composed of an IP address and optional port and includes
* the [root](@ref setRoot()) path component.
*
* In the current implementation at most 2 URLs may be returned - one
* for IPv4 and another for IPv6 protocol (whichever is available), as
* returned by `getAddress()`. Note, however, that the server may be
* also accessible via other IP-addresses/URLs (see `setIpMode()`).
*/
std::vector<std::string> getServerAccessUrls() const;
protected:

View File

@@ -29,10 +29,13 @@
namespace kiwix
{
/**
* An IPv4 and/or IPv6 address.
*/
struct IpAddress
{
std::string addr; // IPv4 address
std::string addr6; // IPv6 address
std::string addr; /**< IPv4 address */
std::string addr6; /**< IPv6 address */
};
typedef std::pair<std::string, std::string> LangNameCodePair;
@@ -263,7 +266,7 @@ std::string getLanguageSelfName(const std::string& lang);
* Slugifies the filename by converting any characters reserved by the operating
* system to '_'. Note filename is only the file name and not a path.
*
* @param filename Valid UTF-8 encoded file name string.
* @param filename Valid UTF-8 encoded file name string.
* @return slugified string.
*/
std::string getSlugifiedFileName(const std::string& filename);

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