1 /*
2  * Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU  General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17  * MA 02110-1301, USA.
18  */
19 
20 #include "internalServer.h"
21 #include <netinet/in.h>
22 
23 #ifdef _WIN32
24 # if !defined(__MINGW32__) && (_MSC_VER < 1600)
25 #   include "stdint4win.h"
26 # endif
27 # include <winsock2.h>
28 # include <ws2tcpip.h>
29 # ifdef __GNUC__
30   // inet_pton is not declared in mingw, even if the function exists.
31   extern "C" {
32     WINSOCK_API_LINKAGE  INT WSAAPI inet_pton( INT Family, PCSTR pszAddrString, PVOID pAddrBuf);
33   }
34 # endif
35   typedef UINT64 uint64_t;
36   typedef UINT16 uint16_t;
37 #endif
38 
39 extern "C" {
40 #include "microhttpd_wrapper.h"
41 }
42 
43 #include "tools/otherTools.h"
44 #include "tools/pathTools.h"
45 #include "tools/regexTools.h"
46 #include "tools/stringTools.h"
47 #include "library.h"
48 #include "name_mapper.h"
49 #include "entry.h"
50 #include "searcher.h"
51 #include "search_renderer.h"
52 #include "opds_dumper.h"
53 
54 #include <zim/uuid.h>
55 
56 #include <mustache.hpp>
57 
58 #include <pthread.h>
59 #include <atomic>
60 #include <string>
61 #include <vector>
62 #include <chrono>
63 #include "kiwixlib-resources.h"
64 
65 #ifndef _WIN32
66 # include <arpa/inet.h>
67 #endif
68 
69 #include "request_context.h"
70 #include "response.h"
71 
72 #define MAX_SEARCH_LEN 140
73 #define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
74 
75 namespace kiwix {
76 
77 static IdNameMapper defaultNameMapper;
78 
79 static MHD_Result staticHandlerCallback(void* cls,
80                                         struct MHD_Connection* connection,
81                                         const char* url,
82                                         const char* method,
83                                         const char* version,
84                                         const char* upload_data,
85                                         size_t* upload_data_size,
86                                         void** cont_cls);
87 
88 
InternalServer(Library * library,NameMapper * nameMapper,std::string addr,int port,std::string root,int nbThreads,bool verbose,bool withTaskbar,bool withLibraryButton,bool blockExternalLinks)89 InternalServer::InternalServer(Library* library,
90                                NameMapper* nameMapper,
91                                std::string addr,
92                                int port,
93                                std::string root,
94                                int nbThreads,
95                                bool verbose,
96                                bool withTaskbar,
97                                bool withLibraryButton,
98                                bool blockExternalLinks) :
99   m_addr(addr),
100   m_port(port),
101   m_root(root),
102   m_nbThreads(nbThreads),
103   m_verbose(verbose),
104   m_withTaskbar(withTaskbar),
105   m_withLibraryButton(withLibraryButton),
106   m_blockExternalLinks(blockExternalLinks),
107   mp_daemon(nullptr),
108   mp_library(library),
109   mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
110 {}
111 
start()112 bool InternalServer::start() {
113 #ifdef _WIN32
114   int flags = MHD_USE_SELECT_INTERNALLY;
115 #else
116   int flags = MHD_USE_POLL_INTERNALLY;
117 #endif
118   if (m_verbose.load())
119     flags |= MHD_USE_DEBUG;
120 
121 
122   struct sockaddr_in sockAddr;
123   memset(&sockAddr, 0, sizeof(sockAddr));
124   sockAddr.sin_family = AF_INET;
125   sockAddr.sin_port = htons(m_port);
126   if (m_addr.empty()) {
127     if (0 != INADDR_ANY)
128       sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
129   } else {
130     if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
131       std::cerr << "Ip address " << m_addr << "  is not a valid ip address" << std::endl;
132       return false;
133     }
134   }
135 
136   mp_daemon = MHD_start_daemon(flags,
137                             m_port,
138                             NULL,
139                             NULL,
140                             &staticHandlerCallback,
141                             this,
142                             MHD_OPTION_SOCK_ADDR, &sockAddr,
143                             MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
144                             MHD_OPTION_END);
145   if (mp_daemon == nullptr) {
146     std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
147               << " is maybe already occupied or need more permissions to be open. "
148                  "Please try as root or with a port number higher or equal to 1024."
149               << std::endl;
150     return false;
151   }
152   auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
153   m_server_id = kiwix::to_string(server_start_time.count());
154   return true;
155 }
156 
stop()157 void InternalServer::stop()
158 {
159   MHD_stop_daemon(mp_daemon);
160 }
161 
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)162 static MHD_Result staticHandlerCallback(void* cls,
163                                         struct MHD_Connection* connection,
164                                         const char* url,
165                                         const char* method,
166                                         const char* version,
167                                         const char* upload_data,
168                                         size_t* upload_data_size,
169                                         void** cont_cls)
170 {
171   InternalServer* _this = static_cast<InternalServer*>(cls);
172 
173   return _this->handlerCallback(connection,
174                                 url,
175                                 method,
176                                 version,
177                                 upload_data,
178                                 upload_data_size,
179                                 cont_cls);
180 }
181 
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)182 MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
183                                            const char* url,
184                                            const char* method,
185                                            const char* version,
186                                            const char* upload_data,
187                                            size_t* upload_data_size,
188                                            void** cont_cls)
189 {
190   auto start_time = std::chrono::steady_clock::now();
191   if (m_verbose.load() ) {
192     printf("======================\n");
193     printf("Requesting : \n");
194     printf("full_url  : %s\n", url);
195   }
196   RequestContext request(connection, m_root, url, method, version);
197 
198   if (m_verbose.load() ) {
199     request.print_debug_info();
200   }
201   /* Unexpected method */
202   if (request.get_method() != RequestMethod::GET
203    && request.get_method() != RequestMethod::POST
204    && request.get_method() != RequestMethod::HEAD) {
205     printf("Reject request because of unhandled request method.\n");
206     printf("----------------------\n");
207     return MHD_NO;
208   }
209 
210   auto response = handle_request(request);
211 
212   if (response->getReturnCode() == MHD_HTTP_INTERNAL_SERVER_ERROR) {
213     printf("========== INTERNAL ERROR !! ============\n");
214     if (!m_verbose.load()) {
215       printf("Requesting : \n");
216       printf("full_url : %s\n", url);
217       request.print_debug_info();
218     }
219   }
220 
221   if (response->getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request))
222     response->set_server_id(m_server_id);
223 
224   auto ret = response->send(request, connection);
225   auto end_time = std::chrono::steady_clock::now();
226   auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
227   if (m_verbose.load()) {
228     printf("Request time : %fs\n", time_span.count());
229     printf("----------------------\n");
230   }
231   return ret;
232 }
233 
handle_request(const RequestContext & request)234 std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
235 {
236   try {
237     if (! request.is_valid_url())
238       return Response::build_404(*this, request, "");
239 
240     const ETag etag = get_matching_if_none_match_etag(request);
241     if ( etag )
242       return Response::build_304(*this, etag);
243 
244     if (kiwix::startsWith(request.get_url(), "/skin/"))
245       return handle_skin(request);
246 
247     if (startsWith(request.get_url(), "/catalog"))
248       return handle_catalog(request);
249 
250     if (request.get_url() == "/meta")
251       return handle_meta(request);
252 
253     if (request.get_url() == "/search")
254       return handle_search(request);
255 
256     if (request.get_url() == "/suggest")
257      return handle_suggest(request);
258 
259     if (request.get_url() == "/random")
260       return handle_random(request);
261 
262     if (request.get_url() == "/catch/external")
263       return handle_captured_external(request);
264 
265     return handle_content(request);
266   } catch (std::exception& e) {
267     fprintf(stderr, "===== Unhandled error : %s\n", e.what());
268     return Response::build_500(*this, e.what());
269   } catch (...) {
270     fprintf(stderr, "===== Unhandled unknown error\n");
271     return Response::build_500(*this, "Unknown error");
272   }
273 }
274 
get_default_data() const275 MustacheData InternalServer::get_default_data() const
276 {
277   MustacheData data;
278   data.set("root", m_root);
279   return data;
280 }
281 
homepage_data() const282 MustacheData InternalServer::homepage_data() const
283 {
284   auto data = get_default_data();
285 
286   MustacheData books{MustacheData::type::list};
287   for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
288     auto& currentBook = mp_library->getBookById(bookId);
289 
290     MustacheData book;
291     book.set("name", mp_nameMapper->getNameForId(bookId));
292     book.set("title", currentBook.getTitle());
293     book.set("description", currentBook.getDescription());
294     book.set("articleCount", beautifyInteger(currentBook.getArticleCount()));
295     book.set("mediaCount", beautifyInteger(currentBook.getMediaCount()));
296     books.push_back(book);
297   }
298 
299   data.set("books", books);
300   return data;
301 }
302 
etag_not_needed(const RequestContext & request) const303 bool InternalServer::etag_not_needed(const RequestContext& request) const
304 {
305   const std::string url = request.get_url();
306   return kiwix::startsWith(url, "/catalog")
307       || url == "/search"
308       || url == "/suggest"
309       || url == "/random"
310       || url == "/catch/external";
311 }
312 
313 ETag
get_matching_if_none_match_etag(const RequestContext & r) const314 InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
315 {
316   try {
317     const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH);
318     return ETag::match(etag_list, m_server_id);
319   } catch (const std::out_of_range&) {
320     return ETag();
321   }
322 }
323 
build_homepage(const RequestContext & request)324 std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
325 {
326   return ContentResponse::build(*this, RESOURCE::templates::index_html, homepage_data(), "text/html; charset=utf-8");
327 }
328 
handle_meta(const RequestContext & request)329 std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)
330 {
331   std::string bookName;
332   std::string bookId;
333   std::string meta_name;
334   std::shared_ptr<Reader> reader;
335   try {
336     bookName = request.get_argument("content");
337     bookId = mp_nameMapper->getIdForName(bookName);
338     meta_name = request.get_argument("name");
339     reader = mp_library->getReaderById(bookId);
340   } catch (const std::out_of_range& e) {
341     return Response::build_404(*this, request, bookName);
342   }
343 
344   if (reader == nullptr) {
345     return Response::build_404(*this, request, bookName);
346   }
347 
348   std::string content;
349   std::string mimeType = "text";
350 
351   if (meta_name == "title") {
352     content = reader->getTitle();
353   } else if (meta_name == "description") {
354     content = reader->getDescription();
355   } else if (meta_name == "language") {
356     content = reader->getLanguage();
357   } else if (meta_name == "name") {
358     content = reader->getName();
359   } else if (meta_name == "tags") {
360     content = reader->getTags();
361   } else if (meta_name == "date") {
362     content = reader->getDate();
363   } else if (meta_name == "creator") {
364     content = reader->getCreator();
365   } else if (meta_name == "publisher") {
366     content = reader->getPublisher();
367   } else if (meta_name == "favicon") {
368     reader->getFavicon(content, mimeType);
369   } else {
370     return Response::build_404(*this, request, bookName);
371   }
372 
373   auto response = ContentResponse::build(*this, content, mimeType);
374   response->set_cacheable();
375   return std::move(response);
376 }
377 
handle_suggest(const RequestContext & request)378 std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
379 {
380   if (m_verbose.load()) {
381     printf("** running handle_suggest\n");
382   }
383 
384   std::string content;
385   std::string mimeType;
386   unsigned int maxSuggestionCount = 10;
387   unsigned int suggestionCount = 0;
388 
389   std::string bookName;
390   std::string bookId;
391   std::string term;
392   std::shared_ptr<Reader> reader;
393   try {
394     bookName = request.get_argument("content");
395     bookId = mp_nameMapper->getIdForName(bookName);
396     term = request.get_argument("term");
397     reader = mp_library->getReaderById(bookId);
398   } catch (const std::out_of_range&) {
399     return Response::build_404(*this, request, bookName);
400   }
401 
402   if (m_verbose.load()) {
403     printf("Searching suggestions for: \"%s\"\n", term.c_str());
404   }
405 
406   MustacheData results{MustacheData::type::list};
407 
408   bool first = true;
409   if (reader != nullptr) {
410     /* Get the suggestions */
411     SuggestionsList_t suggestions;
412     reader->searchSuggestionsSmart(term, maxSuggestionCount, suggestions);
413     for(auto& suggestion:suggestions) {
414       MustacheData result;
415       result.set("label", suggestion[0]);
416       result.set("value", suggestion[0]);
417       result.set("first", first);
418       first = false;
419       results.push_back(result);
420       suggestionCount++;
421     }
422   }
423 
424   /* Propose the fulltext search if possible */
425   if (reader->hasFulltextIndex()) {
426     MustacheData result;
427     result.set("label", "containing '" + term + "'...");
428     result.set("value", term + " ");
429     result.set("first", first);
430     results.push_back(result);
431   }
432 
433   auto data = get_default_data();
434   data.set("suggestions", results);
435 
436   auto response = ContentResponse::build(*this, RESOURCE::templates::suggestion_json, data, "application/json; charset=utf-8");
437   return std::move(response);
438 }
439 
handle_skin(const RequestContext & request)440 std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& request)
441 {
442   if (m_verbose.load()) {
443     printf("** running handle_skin\n");
444   }
445 
446   auto resourceName = request.get_url().substr(1);
447   try {
448     auto response = ContentResponse::build(
449         *this,
450         getResource(resourceName),
451         getMimeTypeForFile(resourceName));
452     response->set_cacheable();
453     return std::move(response);
454   } catch (const ResourceNotFound& e) {
455     return Response::build_404(*this, request, "");
456   }
457 }
458 
handle_search(const RequestContext & request)459 std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& request)
460 {
461   if (m_verbose.load()) {
462     printf("** running handle_search\n");
463   }
464 
465   std::string bookName;
466   std::string bookId;
467   try {
468     bookName = request.get_argument("content");
469     bookId = mp_nameMapper->getIdForName(bookName);
470   } catch (const std::out_of_range&) {}
471 
472   std::string patternString;
473   try {
474     patternString = request.get_argument("pattern");
475   } catch (const std::out_of_range&) {}
476 
477   /* Retrive geo search */
478   bool has_geo_query = false;
479   float latitude = 0;
480   float longitude = 0;
481   float distance = 0;
482   try {
483     latitude = request.get_argument<float>("latitude");
484     longitude = request.get_argument<float>("longitude");
485     distance = request.get_argument<float>("distance");
486     has_geo_query = true;
487   } catch(const std::out_of_range&) {}
488     catch(const std::invalid_argument&) {}
489 
490   std::shared_ptr<Reader> reader(nullptr);
491   try {
492     reader = mp_library->getReaderById(bookId);
493   } catch (const std::out_of_range&) {}
494 
495   /* Try first to load directly the article */
496   if (reader != nullptr && !patternString.empty()) {
497     std::string patternCorrespondingUrl;
498     auto variants = reader->getTitleVariants(patternString);
499     auto variantsItr = variants.begin();
500 
501     while (patternCorrespondingUrl.empty() && variantsItr != variants.end()) {
502       try {
503         auto entry = reader->getEntryFromTitle(*variantsItr);
504         entry = entry.getFinalEntry();
505         patternCorrespondingUrl = entry.getPath();
506         break;
507       } catch(kiwix::NoEntry& e) {
508         variantsItr++;
509       }
510     }
511 
512     /* If article found then redirect directly to it */
513     if (!patternCorrespondingUrl.empty()) {
514       auto redirectUrl = m_root + "/" + bookName + "/" + patternCorrespondingUrl;
515       return Response::build_redirect(*this, redirectUrl);
516     }
517   }
518 
519   /* Make the search */
520   if ( (!reader && !bookName.empty())
521     || (patternString.empty() && ! has_geo_query) ) {
522     auto data = get_default_data();
523     data.set("pattern", encodeDiples(patternString));
524     auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8");
525     response->set_taskbar(bookName, reader ? reader->getTitle() : "");
526     response->set_code(MHD_HTTP_NOT_FOUND);
527     return std::move(response);
528   }
529 
530   Searcher searcher;
531   if (reader) {
532     searcher.add_reader(reader.get());
533   } else {
534     for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
535       auto currentReader = mp_library->getReaderById(bookId);
536       if (currentReader) {
537         searcher.add_reader(currentReader.get());
538       }
539     }
540   }
541 
542   auto start = 0;
543   try {
544     start = request.get_argument<unsigned int>("start");
545   } catch (const std::exception&) {}
546 
547   auto pageLength = 25;
548   try {
549     pageLength = request.get_argument<unsigned int>("pageLength");
550   } catch (const std::exception&) {}
551   if (pageLength > MAX_SEARCH_LEN) {
552     pageLength = MAX_SEARCH_LEN;
553   }
554   if (pageLength == 0) {
555     pageLength = 25;
556   }
557 
558   auto end = start + pageLength;
559 
560   /* Get the results */
561   try {
562     if (patternString.empty()) {
563       searcher.geo_search(latitude, longitude, distance,
564                            start, end, m_verbose.load());
565     } else {
566       searcher.search(patternString,
567                        start, end, m_verbose.load());
568     }
569     SearchRenderer renderer(&searcher, mp_nameMapper);
570     renderer.setSearchPattern(patternString);
571     renderer.setSearchContent(bookName);
572     renderer.setProtocolPrefix(m_root + "/");
573     renderer.setSearchProtocolPrefix(m_root + "/search?");
574     renderer.setPageLength(pageLength);
575     auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
576     response->set_taskbar(bookName, reader ? reader->getTitle() : "");
577     //changing status code if no result obtained
578     if(searcher.getEstimatedResultCount() == 0)
579     {
580       response->set_code(MHD_HTTP_NO_CONTENT);
581     }
582 
583     return std::move(response);
584   } catch (const std::exception& e) {
585     std::cerr << e.what() << std::endl;
586     return Response::build_500(*this, e.what());
587   }
588 }
589 
handle_random(const RequestContext & request)590 std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& request)
591 {
592   if (m_verbose.load()) {
593     printf("** running handle_random\n");
594   }
595 
596   std::string bookName;
597   std::string bookId;
598   std::shared_ptr<Reader> reader;
599   try {
600     bookName = request.get_argument("content");
601     bookId = mp_nameMapper->getIdForName(bookName);
602     reader = mp_library->getReaderById(bookId);
603   } catch (const std::out_of_range&) {
604     return Response::build_404(*this, request, bookName);
605   }
606 
607   if (reader == nullptr) {
608     return Response::build_404(*this, request, bookName);
609   }
610 
611   try {
612     auto entry = reader->getRandomPage();
613     return build_redirect(bookName, entry.getFinalEntry());
614   } catch(kiwix::NoEntry& e) {
615     return Response::build_404(*this, request, bookName);
616   }
617 }
618 
handle_captured_external(const RequestContext & request)619 std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
620 {
621   std::string source = "";
622   try {
623     source = kiwix::urlDecode(request.get_argument("source"));
624   } catch (const std::out_of_range& e) {}
625 
626   if (source.empty())
627     return Response::build_404(*this, request, "");
628 
629   auto data = get_default_data();
630   data.set("source", source);
631   return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
632 }
633 
handle_catalog(const RequestContext & request)634 std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
635 {
636   if (m_verbose.load()) {
637     printf("** running handle_catalog");
638   }
639 
640   std::string host;
641   std::string url;
642   try {
643     host = request.get_header("Host");
644     url  = request.get_url_part(1);
645   } catch (const std::out_of_range&) {
646     return Response::build_404(*this, request, "");
647   }
648 
649   if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
650     return Response::build_404(*this, request, "");
651   }
652 
653   if (url == "searchdescription.xml") {
654     auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
655     return std::move(response);
656   }
657 
658   zim::Uuid uuid;
659   kiwix::OPDSDumper opdsDumper;
660   opdsDumper.setRootLocation(m_root);
661   opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml");
662   opdsDumper.setLibrary(mp_library);
663   std::vector<std::string> bookIdsToDump;
664   if (url == "root.xml") {
665     opdsDumper.setTitle("All zims");
666     uuid = zim::Uuid::generate(host);
667     bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
668   } else if (url == "search") {
669     auto filter = kiwix::Filter().valid(true).local(true).remote(true);
670     string query("<Empty query>");
671     size_t count(10);
672     size_t startIndex(0);
673     try {
674       query = request.get_argument("q");
675       filter.query(query);
676     } catch (const std::out_of_range&) {}
677     try {
678       filter.maxSize(extractFromString<unsigned long>(request.get_argument("maxsize")));
679     } catch (...) {}
680     try {
681       filter.name(request.get_argument("name"));
682     } catch (const std::out_of_range&) {}
683     try {
684       filter.lang(request.get_argument("lang"));
685     } catch (const std::out_of_range&) {}
686     try {
687       count = extractFromString<unsigned long>(request.get_argument("count"));
688     } catch (...) {}
689     try {
690       startIndex = extractFromString<unsigned long>(request.get_argument("start"));
691     } catch (...) {}
692     try {
693       filter.acceptTags(kiwix::split(request.get_argument("tag"), ";"));
694     } catch (...) {}
695     try {
696       filter.rejectTags(kiwix::split(request.get_argument("notag"), ";"));
697     } catch (...) {}
698     opdsDumper.setTitle("Search result for " + query);
699     uuid = zim::Uuid::generate();
700     bookIdsToDump = mp_library->filter(filter);
701     auto totalResults = bookIdsToDump.size();
702     bookIdsToDump.erase(bookIdsToDump.begin(), bookIdsToDump.begin()+startIndex);
703     if (count>0 && bookIdsToDump.size() > count) {
704       bookIdsToDump.resize(count);
705     }
706     opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
707   }
708 
709   opdsDumper.setId(kiwix::to_string(uuid));
710   auto response = ContentResponse::build(
711       *this,
712       opdsDumper.dumpOPDSFeed(bookIdsToDump),
713       "application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
714   return std::move(response);
715 }
716 
717 namespace
718 {
719 
get_book_name(const RequestContext & request)720 std::string get_book_name(const RequestContext& request)
721 {
722   try {
723     return request.get_url_part(0);
724   } catch (const std::out_of_range& e) {
725     return std::string();
726   }
727 }
728 
729 } // unnamed namespace
730 
731 std::shared_ptr<Reader>
get_reader(const std::string & bookName) const732 InternalServer::get_reader(const std::string& bookName) const
733 {
734   std::shared_ptr<Reader> reader;
735   try {
736     const std::string bookId = mp_nameMapper->getIdForName(bookName);
737     reader = mp_library->getReaderById(bookId);
738   } catch (const std::out_of_range& e) {
739   }
740   return reader;
741 }
742 
743 std::unique_ptr<Response>
build_redirect(const std::string & bookName,const kiwix::Entry & entry) const744 InternalServer::build_redirect(const std::string& bookName, const kiwix::Entry& entry) const
745 {
746   auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(entry.getPath());
747   return Response::build_redirect(*this, redirectUrl);
748 }
749 
handle_content(const RequestContext & request)750 std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
751 {
752   if (m_verbose.load()) {
753     printf("** running handle_content\n");
754   }
755 
756   const std::string bookName = get_book_name(request);
757   if (bookName.empty())
758     return build_homepage(request);
759 
760   const std::shared_ptr<Reader> reader = get_reader(bookName);
761   if (reader == nullptr) {
762     return Response::build_404(*this, request, bookName);
763   }
764 
765   auto urlStr = request.get_url().substr(bookName.size()+1);
766   if (urlStr[0] == '/') {
767     urlStr = urlStr.substr(1);
768   }
769 
770   kiwix::Entry entry;
771 
772   try {
773     entry = reader->getEntryFromPath(urlStr);
774     if (entry.isRedirect() || urlStr.empty()) {
775       // If urlStr is empty, we want to mainPage.
776       // We must do a redirection to the real page.
777       return build_redirect(bookName, entry.getFinalEntry());
778     }
779   } catch(kiwix::NoEntry& e) {
780     if (m_verbose.load())
781       printf("Failed to find %s\n", urlStr.c_str());
782 
783     return Response::build_404(*this, request, bookName);
784   }
785 
786   auto response = EntryResponse::build(*this, request, entry);
787   try {
788     dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, reader->getTitle());
789   } catch (std::bad_cast& e) {}
790 
791   if (m_verbose.load()) {
792     printf("Found %s\n", entry.getPath().c_str());
793     printf("mimeType: %s\n", entry.getMimetype().c_str());
794   }
795 
796   return response;
797 }
798 
799 }
800