/* * Copyright 2019 Matthieu Gautier * * 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. */ #include "internalServer.h" #ifdef __FreeBSD__ #include #endif #ifdef _WIN32 # if !defined(__MINGW32__) && (_MSC_VER < 1600) # include "stdint4win.h" # endif # include # include # ifdef __GNUC__ // inet_pton is not declared in mingw, even if the function exists. extern "C" { WINSOCK_API_LINKAGE INT WSAAPI inet_pton( INT Family, PCSTR pszAddrString, PVOID pAddrBuf); } # endif typedef UINT64 uint64_t; typedef UINT16 uint16_t; #endif extern "C" { #include "microhttpd_wrapper.h" } #include "tools.h" #include "tools/pathTools.h" #include "tools/regexTools.h" #include "tools/stringTools.h" #include "tools/archiveTools.h" #include "tools/networkTools.h" #include "library.h" #include "name_mapper.h" #include "entry.h" #include "searcher.h" #include "search_renderer.h" #include "opds_dumper.h" #include #include #include #include #include #include #include #include #include #include #include #include "kiwixlib-resources.h" #ifndef _WIN32 # include #endif #include "request_context.h" #include "response.h" #define MAX_SEARCH_LEN 140 #define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100 namespace kiwix { namespace { inline std::string normalizeRootUrl(std::string rootUrl) { while ( !rootUrl.empty() && rootUrl.back() == '/' ) rootUrl.pop_back(); while ( !rootUrl.empty() && rootUrl.front() == '/' ) rootUrl = rootUrl.substr(1); return rootUrl.empty() ? rootUrl : "/" + rootUrl; } unsigned parseIllustration(const std::string& s) { int nw(0), nh(0), nEnd(0); long int w(-1), h(-1); if ( sscanf(s.c_str(), "Illustration_%n%ldx%n%ld@1%n)", &nw, &w, &nh, &h, &nEnd) == 2 && nEnd == (int)s.size() && !isspace(s[nw]) && !isspace(s[nh]) && w == h && w >= 0) { return w; } return 0; } } // unnamed namespace static IdNameMapper defaultNameMapper; static MHD_Result staticHandlerCallback(void* cls, struct MHD_Connection* connection, const char* url, const char* method, const char* version, const char* upload_data, size_t* upload_data_size, void** cont_cls); InternalServer::InternalServer(Library* library, NameMapper* nameMapper, std::string addr, int port, std::string root, int nbThreads, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, std::string indexTemplateString) : m_addr(addr), m_port(port), m_root(normalizeRootUrl(root)), m_nbThreads(nbThreads), m_verbose(verbose), m_withTaskbar(withTaskbar), m_withLibraryButton(withLibraryButton), m_blockExternalLinks(blockExternalLinks), m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString), mp_daemon(nullptr), mp_library(library), mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper) {} 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 sockAddr; memset(&sockAddr, 0, sizeof(sockAddr)); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(m_port); if (m_addr.empty()) { if (0 != INADDR_ANY) { sockAddr.sin_addr.s_addr = htonl(INADDR_ANY); } m_addr = kiwix::getBestPublicIp(); } else { if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) { std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl; return false; } } 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_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; return false; } auto server_start_time = std::chrono::system_clock::now().time_since_epoch(); m_server_id = kiwix::to_string(server_start_time.count()); m_library_id = m_server_id; return true; } void InternalServer::stop() { MHD_stop_daemon(mp_daemon); } static MHD_Result staticHandlerCallback(void* cls, struct MHD_Connection* connection, const char* url, const char* method, const char* version, const char* upload_data, size_t* upload_data_size, void** cont_cls) { InternalServer* _this = static_cast(cls); return _this->handlerCallback(connection, url, method, version, upload_data, upload_data_size, cont_cls); } MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection, const char* url, const char* method, const char* version, const char* upload_data, size_t* upload_data_size, void** cont_cls) { auto start_time = std::chrono::steady_clock::now(); if (m_verbose.load() ) { printf("======================\n"); printf("Requesting : \n"); printf("full_url : %s\n", url); } RequestContext request(connection, m_root, url, method, version); if (m_verbose.load() ) { request.print_debug_info(); } /* Unexpected method */ if (request.get_method() != RequestMethod::GET && request.get_method() != RequestMethod::POST && request.get_method() != RequestMethod::HEAD) { printf("Reject request because of unhandled request method.\n"); printf("----------------------\n"); return MHD_NO; } auto response = handle_request(request); if (response->getReturnCode() == MHD_HTTP_INTERNAL_SERVER_ERROR) { printf("========== INTERNAL ERROR !! ============\n"); if (!m_verbose.load()) { printf("Requesting : \n"); printf("full_url : %s\n", url); request.print_debug_info(); } } if (response->getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request)) response->set_server_id(m_server_id); auto ret = response->send(request, connection); auto end_time = std::chrono::steady_clock::now(); auto time_span = std::chrono::duration_cast>(end_time - start_time); if (m_verbose.load()) { printf("Request time : %fs\n", time_span.count()); printf("----------------------\n"); } return ret; } std::unique_ptr InternalServer::handle_request(const RequestContext& request) { try { if (! request.is_valid_url()) return Response::build_404(*this, request.get_full_url(), "", ""); const ETag etag = get_matching_if_none_match_etag(request); if ( etag ) return Response::build_304(*this, etag); if (startsWith(request.get_url(), "/skin/")) return handle_skin(request); if (startsWith(request.get_url(), "/catalog/")) return handle_catalog(request); if (startsWith(request.get_url(), "/raw/")) return handle_raw(request); if (request.get_url() == "/meta") return handle_meta(request); if (request.get_url() == "/search") return handle_search(request); if (request.get_url() == "/suggest") return handle_suggest(request); if (request.get_url() == "/random") return handle_random(request); if (request.get_url() == "/catch/external") return handle_captured_external(request); return handle_content(request); } catch (std::exception& e) { fprintf(stderr, "===== Unhandled error : %s\n", e.what()); return Response::build_500(*this, e.what()); } catch (...) { fprintf(stderr, "===== Unhandled unknown error\n"); return Response::build_500(*this, "Unknown error"); } } MustacheData InternalServer::get_default_data() const { MustacheData data; data.set("root", m_root); return data; } bool InternalServer::etag_not_needed(const RequestContext& request) const { const std::string url = request.get_url(); return kiwix::startsWith(url, "/catalog") || url == "/search" || url == "/suggest" || url == "/random" || url == "/catch/external"; } ETag InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const { try { const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH); return ETag::match(etag_list, m_server_id); } catch (const std::out_of_range&) { return ETag(); } } std::unique_ptr InternalServer::build_homepage(const RequestContext& request) { return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8", true); } /** * Archive and Zim handlers begin **/ // TODO: retrieve searcher from caching mechanism SuggestionsList_t getSuggestions(const zim::Archive* const archive, const std::string& queryString, int start, int suggestionCount) { SuggestionsList_t suggestions; auto searcher = zim::SuggestionSearcher(*archive); if (archive->hasTitleIndex()) { auto search = searcher.suggest(queryString); auto srs = search.getResults(start, suggestionCount); for (auto it : srs) { SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()), it.getPath(), it.getSnippet()); suggestions.push_back(suggestion); } } else { // TODO: This case should be handled by libzim std::vector variants = getTitleVariants(queryString); int currCount = 0; for (auto it = variants.begin(); it != variants.end() && currCount < suggestionCount; it++) { auto search = searcher.suggest(queryString); auto srs = search.getResults(0, suggestionCount); for (auto it : srs) { SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()), it.getPath()); suggestions.push_back(suggestion); currCount++; } } } return suggestions; } /** * Archive and Zim handlers end **/ std::unique_ptr InternalServer::handle_meta(const RequestContext& request) { std::string bookName; std::shared_ptr archive; try { bookName = request.get_argument("content"); const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range& e) { // error handled by the archive == nullptr check below } if (archive == nullptr) { const std::string error_details = "No such book: " + bookName; return Response::build_404(*this, "", bookName, "", error_details); } std::string content; std::string mimeType = "text"; const auto meta_name = request.get_optional_param("name", std::string()); if (meta_name == "title") { content = getArchiveTitle(*archive); } else if (meta_name == "description") { content = getMetaDescription(*archive); } else if (meta_name == "language") { content = getMetaLanguage(*archive); } else if (meta_name == "name") { content = getMetaName(*archive); } else if (meta_name == "tags") { content = getMetaTags(*archive); } else if (meta_name == "date") { content = getMetaDate(*archive); } else if (meta_name == "creator") { content = getMetaCreator(*archive); } else if (meta_name == "publisher") { content = getMetaPublisher(*archive); } else if (meta_name == "favicon") { getArchiveFavicon(*archive, 48, content, mimeType); } else if (const unsigned illustrationSize = parseIllustration(meta_name)) { getArchiveFavicon(*archive, illustrationSize, content, mimeType); } else { const std::string error_details = "No such metadata item: " + meta_name; return Response::build_404(*this, "", bookName, "", error_details); } auto response = ContentResponse::build(*this, content, mimeType); response->set_cacheable(); return std::move(response); } std::unique_ptr InternalServer::handle_suggest(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_suggest\n"); } std::string bookName; std::shared_ptr archive; try { bookName = request.get_argument("content"); const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range&) { // error handled by the archive == nullptr check below } if (archive == nullptr) { const std::string error_details = "No such book: " + bookName; return Response::build_404(*this, "", bookName, "", error_details); } const auto queryString = request.get_optional_param("term", std::string()); const auto start = request.get_optional_param("start", 0); unsigned int count = request.get_optional_param("count", 10); if (count == 0) { count = 10; } if (m_verbose.load()) { printf("Searching suggestions for: \"%s\"\n", queryString.c_str()); } MustacheData results{MustacheData::type::list}; bool first = true; /* Get the suggestions */ SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, start, count); for(auto& suggestion:suggestions) { MustacheData result; result.set("label", suggestion.getTitle()); if (suggestion.hasSnippet()) { result.set("label", suggestion.getSnippet()); } result.set("value", suggestion.getTitle()); result.set("kind", "path"); result.set("path", suggestion.getPath()); result.set("first", first); first = false; results.push_back(result); } /* Propose the fulltext search if possible */ if (archive->hasFulltextIndex()) { MustacheData result; result.set("label", "containing '" + queryString + "'..."); result.set("value", queryString + " "); result.set("kind", "pattern"); result.set("first", first); results.push_back(result); } auto data = get_default_data(); data.set("suggestions", results); auto response = ContentResponse::build(*this, RESOURCE::templates::suggestion_json, data, "application/json; charset=utf-8"); return std::move(response); } std::unique_ptr InternalServer::handle_skin(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_skin\n"); } auto resourceName = request.get_url().substr(1); try { auto response = ContentResponse::build( *this, getResource(resourceName), getMimeTypeForFile(resourceName)); response->set_cacheable(); return std::move(response); } catch (const ResourceNotFound& e) { return Response::build_404(*this, request.get_full_url(), "", ""); } } std::unique_ptr InternalServer::handle_search(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_search\n"); } std::string patternString; try { patternString = request.get_argument("pattern"); } catch (const std::out_of_range&) {} /* Retrive geo search */ bool has_geo_query = false; float latitude = 0; float longitude = 0; float distance = 0; try { latitude = request.get_argument("latitude"); longitude = request.get_argument("longitude"); distance = request.get_argument("distance"); has_geo_query = true; } catch(const std::out_of_range&) {} catch(const std::invalid_argument&) {} std::string bookName; std::shared_ptr archive; try { bookName = request.get_argument("content"); const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range&) {} /* Make the search */ if ( (!archive && !bookName.empty()) || (patternString.empty() && ! has_geo_query) ) { auto data = get_default_data(); data.set("pattern", encodeDiples(patternString)); auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8"); response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : ""); response->set_code(MHD_HTTP_NOT_FOUND); return std::move(response); } std::shared_ptr searcher; if (archive) { searcher = std::make_shared(*archive); } else { for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) { auto currentArchive = mp_library->getArchiveById(bookId); if (currentArchive) { if (! searcher) { searcher = std::make_shared(*currentArchive); } else { searcher->addArchive(*currentArchive); } } } } auto start = 0; try { start = request.get_argument("start"); } catch (const std::exception&) {} auto pageLength = 25; try { pageLength = request.get_argument("pageLength"); } catch (const std::exception&) {} if (pageLength > MAX_SEARCH_LEN) { pageLength = MAX_SEARCH_LEN; } if (pageLength == 0) { pageLength = 25; } /* Get the results */ try { zim::Query query; if (patternString.empty()) { // Execute geo-search if (m_verbose.load()) { cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl; } query.setQuery(""); query.setGeorange(latitude, longitude, distance); } else { // Execute Ft search if (m_verbose.load()) { cout << "Performing query `" << patternString << "'" << endl; } std::string queryString = removeAccents(patternString); query.setQuery(queryString); } zim::Search search = searcher->search(query); SearchRenderer renderer(search.getResults(start, pageLength), mp_nameMapper, start, search.getEstimatedMatches()); renderer.setSearchPattern(patternString); renderer.setSearchContent(bookName); renderer.setProtocolPrefix(m_root + "/"); renderer.setSearchProtocolPrefix(m_root + "/search?"); renderer.setPageLength(pageLength); auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8"); response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : ""); return std::move(response); } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return Response::build_500(*this, e.what()); } } std::unique_ptr InternalServer::handle_random(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_random\n"); } std::string bookName; std::shared_ptr archive; try { bookName = request.get_argument("content"); const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range&) { // error handled by the archive == nullptr check below } if (archive == nullptr) { const std::string error_details = "No such book: " + bookName; return Response::build_404(*this, "", bookName, "", error_details); } try { auto entry = archive->getRandomEntry(); return build_redirect(bookName, getFinalItem(*archive, entry)); } catch(zim::EntryNotFound& e) { const std::string error_details = "Oops! Failed to pick a random article :("; return Response::build_404(*this, "", bookName, getArchiveTitle(*archive), error_details); } } std::unique_ptr InternalServer::handle_captured_external(const RequestContext& request) { std::string source = ""; try { source = kiwix::urlDecode(request.get_argument("source")); } catch (const std::out_of_range& e) {} if (source.empty()) return Response::build_404(*this, request.get_full_url(), "", ""); auto data = get_default_data(); data.set("source", source); return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8"); } std::unique_ptr InternalServer::handle_catalog(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_catalog"); } std::string host; std::string url; try { host = request.get_header("Host"); url = request.get_url_part(1); } catch (const std::out_of_range&) { return Response::build_404(*this, request.get_full_url(), "", ""); } if (url == "v2") { return handle_catalog_v2(request); } if (url != "searchdescription.xml" && url != "root.xml" && url != "search") { return Response::build_404(*this, request.get_full_url(), "", ""); } if (url == "searchdescription.xml") { auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml"); return std::move(response); } zim::Uuid uuid; kiwix::OPDSDumper opdsDumper(mp_library); opdsDumper.setRootLocation(m_root); opdsDumper.setLibraryId(m_library_id); std::vector bookIdsToDump; if (url == "root.xml") { uuid = zim::Uuid::generate(host); bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true)); } else if (url == "search") { bookIdsToDump = search_catalog(request, opdsDumper); uuid = zim::Uuid::generate(); } auto response = ContentResponse::build( *this, opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()), "application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8"); return std::move(response); } namespace { Filter get_search_filter(const RequestContext& request) { auto filter = kiwix::Filter().valid(true).local(true); try { filter.query(request.get_argument("q")); } catch (const std::out_of_range&) {} try { filter.maxSize(request.get_argument("maxsize")); } catch (...) {} try { filter.name(request.get_argument("name")); } catch (const std::out_of_range&) {} try { filter.category(request.get_argument("category")); } catch (const std::out_of_range&) {} try { filter.lang(request.get_argument("lang")); } catch (const std::out_of_range&) {} try { filter.acceptTags(kiwix::split(request.get_argument("tag"), ";")); } catch (...) {} try { filter.rejectTags(kiwix::split(request.get_argument("notag"), ";")); } catch (...) {} return filter; } template std::vector subrange(const std::vector& v, size_t s, size_t n) { const size_t e = std::min(v.size(), s+n); return std::vector(v.begin()+std::min(v.size(), s), v.begin()+e); } } // unnamed namespace std::vector InternalServer::search_catalog(const RequestContext& request, kiwix::OPDSDumper& opdsDumper) { const auto filter = get_search_filter(request); const std::string q = filter.hasQuery() ? filter.getQuery() : ""; std::vector bookIdsToDump = mp_library->filter(filter); const auto totalResults = bookIdsToDump.size(); const size_t count = request.get_optional_param("count", 10UL); const size_t startIndex = request.get_optional_param("start", 0UL); bookIdsToDump = subrange(bookIdsToDump, startIndex, count); opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size()); return bookIdsToDump; } namespace { std::string get_book_name(const RequestContext& request) { try { return request.get_url_part(0); } catch (const std::out_of_range& e) { return std::string(); } } std::string searchSuggestionHTML(const std::string& searchURL, const std::string& pattern) { kainjow::mustache::mustache tmpl("Make a full text search for {{pattern}}"); MustacheData data; data.set("pattern", pattern); data.set("searchURL", searchURL); return (tmpl.render(data)); } } // unnamed namespace std::unique_ptr InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const { auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(item.getPath()); return Response::build_redirect(*this, redirectUrl); } std::unique_ptr InternalServer::handle_content(const RequestContext& request) { const std::string url = request.get_url(); const std::string pattern = url.substr((url.find_last_of('/'))+1); if (m_verbose.load()) { printf("** running handle_content\n"); } const std::string bookName = get_book_name(request); if (bookName.empty()) return build_homepage(request); std::shared_ptr archive; try { const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range& e) {} if (archive == nullptr) { std::string searchURL = m_root+"/search?pattern="+pattern; // Make a full search on the entire library. const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern)); return Response::build_404(*this, request.get_full_url(), bookName, "", details); } auto urlStr = request.get_url().substr(bookName.size()+1); if (urlStr[0] == '/') { urlStr = urlStr.substr(1); } try { auto entry = getEntryFromPath(*archive, urlStr); if (entry.isRedirect() || urlStr.empty()) { // If urlStr is empty, we want to mainPage. // We must do a redirection to the real page. return build_redirect(bookName, getFinalItem(*archive, entry)); } auto response = ItemResponse::build(*this, request, entry.getItem()); try { dynamic_cast(*response).set_taskbar(bookName, getArchiveTitle(*archive)); } catch (std::bad_cast& e) {} if (m_verbose.load()) { printf("Found %s\n", entry.getPath().c_str()); printf("mimeType: %s\n", entry.getItem(true).getMimetype().c_str()); } return response; } catch(zim::EntryNotFound& e) { if (m_verbose.load()) printf("Failed to find %s\n", urlStr.c_str()); std::string searchURL = m_root+"/search?content="+bookName+"&pattern="+pattern; // Make a search on this specific book only. const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern)); return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), details); } } std::unique_ptr InternalServer::handle_raw(const RequestContext& request) { if (m_verbose.load()) { printf("** running handle_raw\n"); } std::string bookName; std::string kind; try { bookName = request.get_url_part(1); kind = request.get_url_part(2); } catch (const std::out_of_range& e) { return Response::build_404(*this, request.get_full_url(), bookName, "", ""); } if (kind != "meta" && kind!= "content") { const std::string error_details = kind + " is not a valid request for raw content."; return Response::build_404(*this, request.get_full_url(), bookName, "", error_details); } std::shared_ptr archive; try { const std::string bookId = mp_nameMapper->getIdForName(bookName); archive = mp_library->getArchiveById(bookId); } catch (const std::out_of_range& e) {} if (archive == nullptr) { const std::string error_details = "No such book: " + bookName; return Response::build_404(*this, request.get_full_url(), bookName, "", error_details); } // Remove the beggining of the path: // /raw///foo // ^^^^^ ^ ^ // 5 + 1 + 1 = 7 auto itemPath = request.get_url().substr(bookName.size()+kind.size()+7); try { if (kind == "meta") { auto item = archive->getMetadataItem(itemPath); return ItemResponse::build(*this, request, item, /*raw=*/true); } else { auto entry = archive->getEntryByPath(itemPath); if (entry.isRedirect()) { return build_redirect(bookName, entry.getItem(true)); } return ItemResponse::build(*this, request, entry.getItem(), /*raw=*/true); } } catch (zim::EntryNotFound& e ) { if (m_verbose.load()) { printf("Failed to find %s\n", itemPath.c_str()); } const std::string error_details = "Cannot find " + kind + " entry " + itemPath; return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), error_details); } } }