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