mirror of https://github.com/kiwix/libkiwix.git
Redirection of slashless root URL
With non-empty root location, the canonic form of the root URL for a kiwix server is now required to end with a slash (to match the situation for an empty root location). This requirement enables usage of relative URLs on the welcome page and resources/scripts loaded through that page. A slashless root URL is redirected to the slashful version.
This commit is contained in:
parent
0581da44fe
commit
ac742e9da2
|
@ -97,14 +97,19 @@ inline std::string normalizeRootUrl(std::string rootUrl)
|
||||||
std::string
|
std::string
|
||||||
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
|
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
|
||||||
{
|
{
|
||||||
assert(rootLocation.size() > 0 && rootLocation.back() == '/');
|
|
||||||
if ( kiwix::startsWith(fullUrl, rootLocation) ) {
|
if ( kiwix::startsWith(fullUrl, rootLocation) ) {
|
||||||
return fullUrl.substr(rootLocation.size() - 1);
|
return fullUrl.substr(rootLocation.size());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "INVALID URL";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string getSearchComponent(const RequestContext& request)
|
||||||
|
{
|
||||||
|
const std::string query = request.get_query();
|
||||||
|
return query.empty() ? query : "?" + query;
|
||||||
|
}
|
||||||
|
|
||||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
|
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
|
||||||
{
|
{
|
||||||
auto filter = kiwix::Filter().valid(true).local(true);
|
auto filter = kiwix::Filter().valid(true).local(true);
|
||||||
|
@ -415,7 +420,7 @@ InternalServer::InternalServer(Library* library,
|
||||||
m_addr(addr),
|
m_addr(addr),
|
||||||
m_port(port),
|
m_port(port),
|
||||||
m_root(normalizeRootUrl(root)),
|
m_root(normalizeRootUrl(root)),
|
||||||
m_rootPrefixOfDecodedURL(m_root + "/"),
|
m_rootPrefixOfDecodedURL(m_root),
|
||||||
m_nbThreads(nbThreads),
|
m_nbThreads(nbThreads),
|
||||||
m_multizimSearchLimit(multizimSearchLimit),
|
m_multizimSearchLimit(multizimSearchLimit),
|
||||||
m_verbose(verbose),
|
m_verbose(verbose),
|
||||||
|
@ -585,6 +590,13 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||||
+ urlNotFoundMsg;
|
+ urlNotFoundMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( request.get_url() == "" ) {
|
||||||
|
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
|
||||||
|
// so that relative URLs are resolved correctly
|
||||||
|
const std::string query = getSearchComponent(request);
|
||||||
|
return Response::build_redirect(*this, m_root + "/" + query);
|
||||||
|
}
|
||||||
|
|
||||||
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
||||||
if ( etag )
|
if ( etag )
|
||||||
return Response::build_304(*this, etag);
|
return Response::build_304(*this, etag);
|
||||||
|
@ -623,11 +635,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||||
if (isEndpointUrl(url, "catch"))
|
if (isEndpointUrl(url, "catch"))
|
||||||
return handle_catch(request);
|
return handle_catch(request);
|
||||||
|
|
||||||
std::string contentUrl = m_root + "/content" + urlEncode(url);
|
const std::string contentUrl = m_root + "/content" + urlEncode(url);
|
||||||
const std::string query = request.get_query();
|
const std::string query = getSearchComponent(request);
|
||||||
if ( ! query.empty() )
|
return Response::build_redirect(*this, contentUrl + query);
|
||||||
contentUrl += "?" + query;
|
|
||||||
return Response::build_redirect(*this, contentUrl);
|
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||||
return HTTP500Response(*this, request)
|
return HTTP500Response(*this, request)
|
||||||
|
|
|
@ -181,7 +181,7 @@ std::string RequestContext::get_root_path() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RequestContext::is_valid_url() const {
|
bool RequestContext::is_valid_url() const {
|
||||||
return !url.empty();
|
return url.empty() || url[0] == '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteRange RequestContext::get_range() const {
|
ByteRange RequestContext::get_range() const {
|
||||||
|
|
|
@ -359,6 +359,10 @@ TEST_F(ServerTest, 400)
|
||||||
const char* urls404[] = {
|
const char* urls404[] = {
|
||||||
"/",
|
"/",
|
||||||
"/zimfile",
|
"/zimfile",
|
||||||
|
"/ROOT",
|
||||||
|
"/ROOT%23%",
|
||||||
|
"/ROOT%23%3",
|
||||||
|
"/ROOT%23%3Fxyz",
|
||||||
"/ROOT%23%3F/skin/non-existent-skin-resource",
|
"/ROOT%23%3F/skin/non-existent-skin-resource",
|
||||||
"/ROOT%23%3F/skin/autoComplete.min.js?cacheid=wrongcacheid",
|
"/ROOT%23%3F/skin/autoComplete.min.js?cacheid=wrongcacheid",
|
||||||
"/ROOT%23%3F/catalog",
|
"/ROOT%23%3F/catalog",
|
||||||
|
@ -1268,6 +1272,36 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ServerTest, SlashlessRootURLIsRedirectedToSlashfulURL)
|
||||||
|
{
|
||||||
|
const std::pair<const char*, const char*> test_data[] = {
|
||||||
|
// URL redirect
|
||||||
|
{ "/ROOT%23%3F", "/ROOT%23%3F/" },
|
||||||
|
{ "/ROOT%23%3F?abcd=123&xyz=890", "/ROOT%23%3F/?abcd=123&xyz=890" }
|
||||||
|
};
|
||||||
|
|
||||||
|
for ( const auto& t : test_data )
|
||||||
|
{
|
||||||
|
const TestContext ctx{ {"url", t.first} };
|
||||||
|
const auto g = zfs1_->GET(t.first);
|
||||||
|
ASSERT_EQ(302, g->status) << ctx;
|
||||||
|
ASSERT_TRUE(g->has_header("Location")) << ctx;
|
||||||
|
ASSERT_EQ(g->get_header_value("Location"), t.second) << ctx;
|
||||||
|
ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate") << ctx;
|
||||||
|
ASSERT_FALSE(g->has_header("ETag")) << ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ServerTest, EmptyRootIsNotRedirected)
|
||||||
|
{
|
||||||
|
ZimFileServer::Cfg serverCfg;
|
||||||
|
serverCfg.root = "";
|
||||||
|
|
||||||
|
resetServer(serverCfg);
|
||||||
|
|
||||||
|
ASSERT_EQ(200, zfs1_->GET("/")->status);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
|
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
|
||||||
{
|
{
|
||||||
auto g = zfs1_->GET("/ROOT%23%3F/random?content=zimfile");
|
auto g = zfs1_->GET("/ROOT%23%3F/random?content=zimfile");
|
||||||
|
|
|
@ -68,10 +68,18 @@ public: // types
|
||||||
DEFAULT_OPTIONS = WITH_TASKBAR | WITH_LIBRARY_BUTTON
|
DEFAULT_OPTIONS = WITH_TASKBAR | WITH_LIBRARY_BUTTON
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Cfg
|
||||||
|
{
|
||||||
|
std::string root = "ROOT#?";
|
||||||
|
Options options = DEFAULT_OPTIONS;
|
||||||
|
|
||||||
|
Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {}
|
||||||
|
};
|
||||||
|
|
||||||
public: // functions
|
public: // functions
|
||||||
ZimFileServer(int serverPort, Options options, std::string libraryFilePath);
|
ZimFileServer(int serverPort, Cfg cfg, std::string libraryFilePath);
|
||||||
ZimFileServer(int serverPort,
|
ZimFileServer(int serverPort,
|
||||||
Options options,
|
Cfg cfg,
|
||||||
const FilePathCollection& zimpaths,
|
const FilePathCollection& zimpaths,
|
||||||
std::string indexTemplateString = "");
|
std::string indexTemplateString = "");
|
||||||
~ZimFileServer();
|
~ZimFileServer();
|
||||||
|
@ -95,12 +103,12 @@ private: // data
|
||||||
std::unique_ptr<kiwix::NameMapper> nameMapper;
|
std::unique_ptr<kiwix::NameMapper> nameMapper;
|
||||||
std::unique_ptr<kiwix::Server> server;
|
std::unique_ptr<kiwix::Server> server;
|
||||||
std::unique_ptr<httplib::Client> client;
|
std::unique_ptr<httplib::Client> client;
|
||||||
const Options options = DEFAULT_OPTIONS;
|
const Cfg cfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libraryFilePath)
|
ZimFileServer::ZimFileServer(int serverPort, Cfg _cfg, std::string libraryFilePath)
|
||||||
: manager(&this->library)
|
: manager(&this->library)
|
||||||
, options(_options)
|
, cfg(_cfg)
|
||||||
{
|
{
|
||||||
if ( kiwix::isRelativePath(libraryFilePath) )
|
if ( kiwix::isRelativePath(libraryFilePath) )
|
||||||
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
|
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
|
||||||
|
@ -109,11 +117,11 @@ ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libra
|
||||||
}
|
}
|
||||||
|
|
||||||
ZimFileServer::ZimFileServer(int serverPort,
|
ZimFileServer::ZimFileServer(int serverPort,
|
||||||
Options _options,
|
Cfg _cfg,
|
||||||
const FilePathCollection& zimpaths,
|
const FilePathCollection& zimpaths,
|
||||||
std::string indexTemplateString)
|
std::string indexTemplateString)
|
||||||
: manager(&this->library)
|
: manager(&this->library)
|
||||||
, options(_options)
|
, cfg(_cfg)
|
||||||
{
|
{
|
||||||
for ( const auto& zimpath : zimpaths ) {
|
for ( const auto& zimpath : zimpaths ) {
|
||||||
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
|
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
|
||||||
|
@ -125,19 +133,19 @@ ZimFileServer::ZimFileServer(int serverPort,
|
||||||
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||||
{
|
{
|
||||||
const std::string address = "127.0.0.1";
|
const std::string address = "127.0.0.1";
|
||||||
if (options & NO_NAME_MAPPER) {
|
if (cfg.options & NO_NAME_MAPPER) {
|
||||||
nameMapper.reset(new kiwix::IdNameMapper());
|
nameMapper.reset(new kiwix::IdNameMapper());
|
||||||
} else {
|
} else {
|
||||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
||||||
}
|
}
|
||||||
server.reset(new kiwix::Server(&library, nameMapper.get()));
|
server.reset(new kiwix::Server(&library, nameMapper.get()));
|
||||||
server->setRoot("ROOT#?");
|
server->setRoot(cfg.root);
|
||||||
server->setAddress(address);
|
server->setAddress(address);
|
||||||
server->setPort(serverPort);
|
server->setPort(serverPort);
|
||||||
server->setNbThreads(2);
|
server->setNbThreads(2);
|
||||||
server->setVerbose(false);
|
server->setVerbose(false);
|
||||||
server->setTaskbar(options & WITH_TASKBAR, options & WITH_LIBRARY_BUTTON);
|
server->setTaskbar(cfg.options & WITH_TASKBAR, cfg.options & WITH_LIBRARY_BUTTON);
|
||||||
server->setBlockExternalLinks(options & BLOCK_EXTERNAL_LINKS);
|
server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS);
|
||||||
server->setMultiZimSearchLimit(3);
|
server->setMultiZimSearchLimit(3);
|
||||||
if (!indexTemplateString.empty()) {
|
if (!indexTemplateString.empty()) {
|
||||||
server->setIndexTemplateString(indexTemplateString);
|
server->setIndexTemplateString(indexTemplateString);
|
||||||
|
@ -171,9 +179,9 @@ protected:
|
||||||
resetServer(ZimFileServer::DEFAULT_OPTIONS);
|
resetServer(ZimFileServer::DEFAULT_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetServer(ZimFileServer::Options options) {
|
void resetServer(ZimFileServer::Cfg cfg) {
|
||||||
zfs1_.reset();
|
zfs1_.reset();
|
||||||
zfs1_.reset(new ZimFileServer(SERVER_PORT, options, ZIMFILES));
|
zfs1_.reset(new ZimFileServer(SERVER_PORT, cfg, ZIMFILES));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
|
|
Loading…
Reference in New Issue