1 
2 
3 
4 #include "response.h"
5 #include "request_context.h"
6 #include "internalServer.h"
7 #include "kiwixlib-resources.h"
8 
9 #include "tools/regexTools.h"
10 #include "tools/stringTools.h"
11 
12 #include "string.h"
13 #include <mustache.hpp>
14 #include <zlib.h>
15 
16 
17 #define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
18 
19 namespace kiwix {
20 
21 namespace
22 {
23 // some utilities
24 
render_template(const std::string & template_str,kainjow::mustache::data data)25 std::string render_template(const std::string& template_str, kainjow::mustache::data data)
26 {
27   kainjow::mustache::mustache tmpl(template_str);
28   kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
29                                [](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
30   data.set("urlencoded", urlencode);
31   std::stringstream ss;
32   tmpl.render(data, [&ss](const std::string& str) { ss << str; });
33   return ss.str();
34 }
35 
get_mime_type(const kiwix::Entry & entry)36 std::string get_mime_type(const kiwix::Entry& entry)
37 {
38   try {
39     return entry.getMimetype();
40   } catch (exception& e) {
41     return "application/octet-stream";
42   }
43 }
44 
is_compressible_mime_type(const std::string & mimeType)45 bool is_compressible_mime_type(const std::string& mimeType)
46 {
47   return mimeType.find("text/") != string::npos
48       || mimeType.find("application/javascript") != string::npos
49       || mimeType.find("application/atom") != string::npos
50       || mimeType.find("application/opensearchdescription") != string::npos
51       || mimeType.find("application/json") != string::npos;
52 }
53 
54 
55 } // unnamed namespace
56 
Response(bool verbose)57 Response::Response(bool verbose)
58   : m_verbose(verbose),
59     m_returnCode(MHD_HTTP_OK)
60 {
61   add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
62 }
63 
build(const InternalServer & server)64 std::unique_ptr<Response> Response::build(const InternalServer& server)
65 {
66   return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
67 }
68 
build_304(const InternalServer & server,const ETag & etag)69 std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
70 {
71   auto response = Response::build(server);
72   response->set_code(MHD_HTTP_NOT_MODIFIED);
73   response->m_etag = etag;
74   if ( etag.get_option(ETag::COMPRESSED_CONTENT) ) {
75     response->add_header(MHD_HTTP_HEADER_VARY, "Accept-Encoding");
76   }
77   return response;
78 }
79 
build_404(const InternalServer & server,const RequestContext & request,const std::string & bookName)80 std::unique_ptr<Response> Response::build_404(const InternalServer& server, const RequestContext& request, const std::string& bookName)
81 {
82   MustacheData results;
83   results.set("url", request.get_full_url());
84 
85   auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html");
86   response->set_code(MHD_HTTP_NOT_FOUND);
87   response->set_taskbar(bookName, "");
88 
89   return std::move(response);
90 }
91 
build_416(const InternalServer & server,size_t resourceLength)92 std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
93 {
94   auto response = Response::build(server);
95 // [FIXME] (compile with recent enough version of libmicrohttpd)
96 //  response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
97   response->set_code(416);
98   std::ostringstream oss;
99   oss << "bytes */" << resourceLength;
100   response->add_header(MHD_HTTP_HEADER_CONTENT_RANGE, oss.str());
101 
102   return response;
103 }
104 
build_500(const InternalServer & server,const std::string & msg)105 std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg)
106 {
107   MustacheData data;
108   data.set("error", msg);
109   auto content = render_template(RESOURCE::templates::_500_html, data);
110   std::unique_ptr<Response> response (
111       new ContentResponse(server.m_root, true, false, false, false, content, "text/html"));
112   response->set_code(MHD_HTTP_INTERNAL_SERVER_ERROR);
113   return response;
114 }
115 
116 
build_redirect(const InternalServer & server,const std::string & redirectUrl)117 std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
118 {
119   auto response = Response::build(server);
120   response->m_returnCode = MHD_HTTP_FOUND;
121   response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
122   return response;
123 }
124 
print_key_value(void * cls,enum MHD_ValueKind kind,const char * key,const char * value)125 static MHD_Result print_key_value (void *cls, enum MHD_ValueKind kind,
126                                    const char *key, const char *value)
127 {
128   printf (" - %s: '%s'\n", key, value);
129   return MHD_YES;
130 }
131 
132 
133 struct RunningResponse {
134    kiwix::Entry entry;
135    int range_start;
136 
RunningResponsekiwix::RunningResponse137    RunningResponse(kiwix::Entry entry,
138                    int range_start) :
139      entry(entry),
140      range_start(range_start)
141    {}
142 };
143 
callback_reader_from_entry(void * cls,uint64_t pos,char * buf,size_t max)144 static ssize_t callback_reader_from_entry(void* cls,
145                                   uint64_t pos,
146                                   char* buf,
147                                   size_t max)
148 {
149   RunningResponse* response = static_cast<RunningResponse*>(cls);
150 
151   size_t max_size_to_set = min<size_t>(
152     max,
153     response->entry.getSize() - pos - response->range_start);
154 
155   if (max_size_to_set <= 0) {
156     return MHD_CONTENT_READER_END_WITH_ERROR;
157   }
158 
159   zim::Blob blob = response->entry.getBlob(response->range_start+pos, max_size_to_set);
160   memcpy(buf, blob.data(), max_size_to_set);
161   return max_size_to_set;
162 }
163 
callback_free_response(void * cls)164 static void callback_free_response(void* cls)
165 {
166   RunningResponse* response = static_cast<RunningResponse*>(cls);
167   delete response;
168 }
169 
170 
171 
print_response_info(int retCode,MHD_Response * response)172 void print_response_info(int retCode, MHD_Response* response)
173 {
174   printf("Response :\n");
175   printf("httpResponseCode : %d\n", retCode);
176   printf("headers :\n");
177   MHD_get_response_headers(response, print_key_value, nullptr);
178 }
179 
180 
181 
182 
introduce_taskbar()183 void ContentResponse::introduce_taskbar()
184 {
185   kainjow::mustache::data data;
186   data.set("root", m_root);
187   data.set("content", m_bookName);
188   data.set("hascontent", !m_bookName.empty());
189   data.set("title", m_bookTitle);
190   data.set("withlibrarybutton", m_withLibraryButton);
191   auto head_content = render_template(RESOURCE::templates::head_part_html, data);
192   m_content = appendToFirstOccurence(
193     m_content,
194     "<head>",
195     head_content);
196 
197   auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data);
198   m_content = appendToFirstOccurence(
199     m_content,
200     "<body[^>]*>",
201     taskbar_part);
202 }
203 
204 
inject_externallinks_blocker()205 void ContentResponse::inject_externallinks_blocker()
206 {
207   kainjow::mustache::data data;
208   data.set("root", m_root);
209   auto script_tag = render_template(RESOURCE::templates::external_blocker_part_html, data);
210   m_content = appendToFirstOccurence(
211     m_content,
212     "<head>",
213     script_tag);
214 }
215 
216 bool
can_compress(const RequestContext & request) const217 ContentResponse::can_compress(const RequestContext& request) const
218 {
219   return request.can_compress()
220       && is_compressible_mime_type(m_mimeType)
221       && (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
222 }
223 
224 bool
contentDecorationAllowed() const225 ContentResponse::contentDecorationAllowed() const
226 {
227     return (startsWith(m_mimeType, "text/html")
228          && m_mimeType.find(";raw=true") == std::string::npos);
229 }
230 
231 MHD_Response*
create_mhd_response(const RequestContext & request)232 Response::create_mhd_response(const RequestContext& request)
233 {
234   MHD_Response* response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT);
235   return response;
236 }
237 
238 MHD_Response*
create_mhd_response(const RequestContext & request)239 ContentResponse::create_mhd_response(const RequestContext& request)
240 {
241   if (contentDecorationAllowed()) {
242     if (m_withTaskbar) {
243       introduce_taskbar();
244     }
245     if (m_blockExternalLinks) {
246       inject_externallinks_blocker();
247     }
248   }
249 
250   bool shouldCompress = can_compress(request);
251   if (shouldCompress) {
252     std::vector<Bytef> compr_buffer(compressBound(m_content.size()));
253     uLongf comprLen = compr_buffer.capacity();
254     int err = compress(&compr_buffer[0],
255                        &comprLen,
256                        (const Bytef*)(m_content.data()),
257                        m_content.size());
258     if (err == Z_OK && comprLen > 2 && comprLen < (m_content.size() + 2)) {
259       /* /!\ Internet Explorer has a bug with deflate compression.
260          It can not handle the first two bytes (compression headers)
261          We need to chunk them off (move the content 2bytes)
262          It has no incidence on other browsers
263          See http://www.subbu.org/blog/2008/03/ie7-deflate-or-not and comments */
264       m_content = string((char*)&compr_buffer[2], comprLen - 2);
265       m_etag.set_option(ETag::COMPRESSED_CONTENT);
266     } else {
267       shouldCompress = false;
268     }
269   }
270 
271   MHD_Response* response = MHD_create_response_from_buffer(
272     m_content.size(), const_cast<char*>(m_content.data()), MHD_RESPMEM_MUST_COPY);
273 
274   if (shouldCompress) {
275     MHD_add_response_header(
276         response, MHD_HTTP_HEADER_VARY, "Accept-Encoding");
277     MHD_add_response_header(
278         response, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate");
279   }
280   return response;
281 }
282 
send(const RequestContext & request,MHD_Connection * connection)283 MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
284 {
285   MHD_Response* response = create_mhd_response(request);
286 
287   MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
288     m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
289   const std::string etag = m_etag.get_etag();
290   if ( ! etag.empty() )
291     MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
292   for(auto& p: m_customHeaders) {
293     MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
294   }
295 
296   if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
297     m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
298 
299   if (m_verbose)
300     print_response_info(m_returnCode, response);
301 
302   auto ret = MHD_queue_response(connection, m_returnCode, response);
303   MHD_destroy_response(response);
304   return ret;
305 }
306 
set_taskbar(const std::string & bookName,const std::string & bookTitle)307 void ContentResponse::set_taskbar(const std::string& bookName, const std::string& bookTitle)
308 {
309   m_bookName = bookName;
310   m_bookTitle = bookTitle;
311 }
312 
313 
ContentResponse(const std::string & root,bool verbose,bool withTaskbar,bool withLibraryButton,bool blockExternalLinks,const std::string & content,const std::string & mimetype)314 ContentResponse::ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype) :
315   Response(verbose),
316   m_root(root),
317   m_content(content),
318   m_mimeType(mimetype),
319   m_withTaskbar(withTaskbar),
320   m_withLibraryButton(withLibraryButton),
321   m_blockExternalLinks(blockExternalLinks),
322   m_bookName(""),
323   m_bookTitle("")
324 {
325   add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
326 }
327 
build(const InternalServer & server,const std::string & content,const std::string & mimetype)328 std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype)
329 {
330    return std::unique_ptr<ContentResponse>(new ContentResponse(
331         server.m_root,
332         server.m_verbose.load(),
333         server.m_withTaskbar,
334         server.m_withLibraryButton,
335         server.m_blockExternalLinks,
336         content,
337         mimetype));
338 }
339 
build(const InternalServer & server,const std::string & template_str,kainjow::mustache::data data,const std::string & mimetype)340 std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype) {
341   auto content = render_template(template_str, data);
342   return ContentResponse::build(server, content, mimetype);
343 }
344 
EntryResponse(bool verbose,const Entry & entry,const std::string & mimetype,const ByteRange & byterange)345 EntryResponse::EntryResponse(bool verbose, const Entry& entry, const std::string& mimetype, const ByteRange& byterange) :
346   Response(verbose),
347   m_entry(entry),
348   m_mimeType(mimetype)
349 {
350   m_byteRange = byterange;
351   set_cacheable();
352   add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
353 }
354 
build(const InternalServer & server,const RequestContext & request,const Entry & entry)355 std::unique_ptr<Response> EntryResponse::build(const InternalServer& server, const RequestContext& request, const Entry& entry)
356 {
357   const std::string mimetype = get_mime_type(entry);
358   auto byteRange = request.get_range().resolve(entry.getSize());
359   const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
360   if (noRange && is_compressible_mime_type(mimetype)) {
361     // Return a contentResponse
362     zim::Blob raw_content = entry.getBlob();
363     const std::string content = string(raw_content.data(), raw_content.size());
364     auto response = ContentResponse::build(server, content, mimetype);
365     response->set_cacheable();
366     response->m_byteRange = byteRange;
367     return std::move(response);
368   }
369 
370   if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
371     auto response = Response::build_416(server, entry.getSize());
372     response->set_cacheable();
373     return response;
374   }
375 
376   return std::unique_ptr<Response>(new EntryResponse(
377         server.m_verbose.load(),
378         entry,
379         mimetype,
380         byteRange));
381 }
382 
383 MHD_Response*
create_mhd_response(const RequestContext & request)384 EntryResponse::create_mhd_response(const RequestContext& request)
385 {
386   const auto content_length = m_byteRange.length();
387   MHD_Response* response = MHD_create_response_from_callback(content_length,
388                                                16384,
389                                                callback_reader_from_entry,
390                                                new RunningResponse(m_entry, m_byteRange.first()),
391                                                callback_free_response);
392   MHD_add_response_header(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
393   if ( m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT ) {
394     std::ostringstream oss;
395     oss << "bytes " << m_byteRange.first() << "-" << m_byteRange.last()
396         << "/" << m_entry.getSize();
397 
398     MHD_add_response_header(response,
399       MHD_HTTP_HEADER_CONTENT_RANGE, oss.str().c_str());
400   }
401 
402   MHD_add_response_header(response,
403     MHD_HTTP_HEADER_CONTENT_LENGTH, kiwix::to_string(content_length).c_str());
404   return response;
405 }
406 
407 
408 }
409