Fawkes API  Fawkes Development Version
request_dispatcher.cpp
1 
2 /***************************************************************************
3  * request_dispatcher.cpp - Web request dispatcher
4  *
5  * Created: Mon Oct 13 22:48:04 2008
6  * Copyright 2006-2018 Tim Niemueller [www.niemueller.de]
7  ****************************************************************************/
8 
9 /* This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU Library General Public License for more details.
18  *
19  * Read the full text in the LICENSE.GPL file in the doc directory.
20  */
21 
22 #include "microhttpd_compat.h"
23 
24 #include <core/exception.h>
25 #include <core/threading/mutex.h>
26 #include <core/threading/mutex_locker.h>
27 #include <sys/socket.h>
28 #include <sys/types.h>
29 #include <utils/misc/string_urlescape.h>
30 #include <utils/time/time.h>
31 #include <webview/access_log.h>
32 #include <webview/error_reply.h>
33 #include <webview/page_reply.h>
34 #include <webview/request_dispatcher.h>
35 #include <webview/url_manager.h>
36 #include <webview/user_verifier.h>
37 
38 #include <cstdarg>
39 #include <cstdlib>
40 #include <cstring>
41 
42 #define UNAUTHORIZED_REPLY \
43  "<html>\n" \
44  " <head><title>Access denied</title></head>\n" \
45  " <body>\n" \
46  " <h1>Access denied</h1>\n" \
47  " <p>Authentication is required to access Fawkes Webview</p>\n" \
48  " </body>\n" \
49  "</html>"
50 
51 namespace fawkes {
52 
53 /** @class WebRequestDispatcher "request_dispatcher.h"
54  * Web request dispatcher.
55  * Takes web request received via a webserver run by libmicrohttpd and dispatches
56  * pages to registered URL handlers or gives a 404 error if no
57  * handler was registered for the given url.
58  * @author Tim Niemueller
59  */
60 
61 /** Constructor.
62  * @param url_manager URL manager to use for URL to processor mapping
63  * @param headergen page header generator
64  * @param footergen page footer generator
65  */
67  WebPageHeaderGenerator *headergen,
68  WebPageFooterGenerator *footergen)
69 {
70  realm_ = NULL;
71  access_log_ = NULL;
72  url_manager_ = url_manager;
73  page_header_generator_ = headergen;
74  page_footer_generator_ = footergen;
75  active_requests_ = 0;
76  active_requests_mutex_ = new Mutex();
77  last_request_completion_time_ = new Time();
78 
79  cors_allow_all_ = false;
80  cors_max_age_ = 0;
81 }
82 
83 /** Destructor. */
85 {
86  if (realm_)
87  free(realm_);
88  delete active_requests_mutex_;
89  delete last_request_completion_time_;
90  delete access_log_;
91 }
92 
93 /** Setup basic authentication.
94  * @param realm authentication realm to display to the user.
95  * If NULL basic authentication will be disabled.
96  * @param verifier verifier to use for checking credentials.
97  * If NULL basic authentication will be disabled.
98  */
99 void
101 {
102 #if MHD_VERSION >= 0x00090400
103  if (realm_)
104  free(realm_);
105  realm_ = NULL;
106  user_verifier_ = NULL;
107  if (realm && verifier) {
108  realm_ = strdup(realm);
109  user_verifier_ = verifier;
110  }
111 #else
112  throw Exception("libmicrohttpd >= 0.9.4 is required for basic authentication, "
113  "which was not available at compile time.");
114 #endif
115 }
116 
117 /** Setup access log.
118  * @param filename access log file name
119  */
120 void
122 {
123  delete access_log_;
124  access_log_ = NULL;
125  access_log_ = new WebviewAccessLog(filename);
126 }
127 
128 /** Setup cross-origin resource sharing
129  * @param allow_all allow access to all hosts
130  * @param origins allow access from these specific origins
131  * @param max_age maximum cache time to send to the client, zero to disable
132  */
133 void
135  std::vector<std::string> &&origins,
136  unsigned int max_age)
137 {
138  cors_allow_all_ = allow_all;
139  cors_origins_ = std::move(origins);
140  cors_max_age_ = max_age;
141 }
142 
143 /** Callback for new requests.
144  * @param cls closure, must be WebRequestDispatcher
145  * @param uri requested URI
146  * @return returns output of WebRequestDispatcher::log_uri()
147  */
148 void *
149 WebRequestDispatcher::uri_log_cb(void *cls, const char *uri)
150 {
151  WebRequestDispatcher *rd = static_cast<WebRequestDispatcher *>(cls);
152  return rd->log_uri(uri);
153 }
154 
155 /** Process request callback for libmicrohttpd.
156  * @param callback_data instance of WebRequestDispatcher to call
157  * @param connection libmicrohttpd connection instance
158  * @param url URL, may contain escape sequences
159  * @param method HTTP method
160  * @param version HTTP version
161  * @param upload_data uploaded data
162  * @param upload_data_size size of upload_data parameter
163  * @param session_data session data pointer
164  * @return appropriate return code for libmicrohttpd
165  */
166 MHD_RESULT
168  struct MHD_Connection *connection,
169  const char * url,
170  const char * method,
171  const char * version,
172  const char * upload_data,
173  size_t * upload_data_size,
174  void ** session_data)
175 {
176  WebRequestDispatcher *rd = static_cast<WebRequestDispatcher *>(callback_data);
177  return rd->process_request(
178  connection, url, method, version, upload_data, upload_data_size, session_data);
179 }
180 
181 /** Process request completion.
182  * @param cls closure which is a pointer to the request dispatcher
183  * @param connection connection on which the request completed
184  * @param con_cls connection specific data, for us the request
185  * @param toe termination code
186  */
187 void
189  struct MHD_Connection * connection,
190  void ** con_cls,
191  enum MHD_RequestTerminationCode toe)
192 {
193  WebRequestDispatcher *rd = static_cast<WebRequestDispatcher *>(cls);
194  WebRequest * request = static_cast<WebRequest *>(*con_cls);
195  rd->request_completed(request, toe);
196  delete request;
197 }
198 
199 /** Callback based chunk-wise data.
200  * Supplies data chunk based.
201  * @param reply instance of DynamicWebReply
202  * @param pos position in stream
203  * @param buf buffer to put data in
204  * @param max maximum number of bytes that can be put in buf
205  * @return suitable libmicrohttpd return code
206  */
207 static ssize_t
208 dynamic_reply_data_cb(void *reply, uint64_t pos, char *buf, size_t max)
209 {
210  DynamicWebReply *dreply = static_cast<DynamicWebReply *>(reply);
211  ssize_t bytes = dreply->next_chunk(pos, buf, max);
212  WebRequest * request = dreply->get_request();
213  if (bytes > 0 && request)
214  request->increment_reply_size(bytes);
215  return bytes;
216 }
217 
218 /** Callback to free dynamic web reply.
219  * @param reply Instance of DynamicWebReply to free.
220  */
221 static void
223 {
224  DynamicWebReply *dreply = static_cast<DynamicWebReply *>(reply);
225  delete dreply;
226 }
227 
228 /** Prepare response from static reply.
229  * @param sreply static reply
230  * @return response struct ready to be enqueued
231  */
232 struct MHD_Response *
233 WebRequestDispatcher::prepare_static_response(StaticWebReply *sreply)
234 {
235  struct MHD_Response *response;
236  WebPageReply * wpreply = dynamic_cast<WebPageReply *>(sreply);
237  if (wpreply) {
238  wpreply->pack(active_baseurl_, page_header_generator_, page_footer_generator_);
239  } else {
240  sreply->pack_caching();
241  sreply->pack();
242  }
243  if (sreply->body_length() > 0) {
244  response = MHD_create_response_from_buffer(sreply->body_length(),
245  (void *)sreply->body().c_str(),
246  MHD_RESPMEM_MUST_COPY);
247  } else {
248  response = MHD_create_response_from_buffer(0, (void *)"", MHD_RESPMEM_PERSISTENT);
249  }
250 
251  WebRequest *request = sreply->get_request();
252  if (request) {
253  request->set_reply_code(sreply->code());
254  request->increment_reply_size(sreply->body_length());
255  }
256 
257  const WebReply::HeaderMap & headers = sreply->headers();
258  WebReply::HeaderMap::const_iterator i;
259  for (i = headers.begin(); i != headers.end(); ++i) {
260  MHD_add_response_header(response, i->first.c_str(), i->second.c_str());
261  }
262 
263  return response;
264 }
265 
266 /** Prepare response from static reply.
267  * @param request request this reply is associated to
268  * @param sreply static reply
269  * @return response struct ready to be enqueued
270  */
271 MHD_RESULT
272 WebRequestDispatcher::queue_dynamic_reply(struct MHD_Connection *connection,
273  WebRequest * request,
274  DynamicWebReply * dreply)
275 {
276  dreply->set_request(request);
277  dreply->pack_caching();
278  request->set_reply_code(dreply->code());
279 
280  struct MHD_Response *response;
281  response = MHD_create_response_from_callback(
282  dreply->size(), dreply->chunk_size(), dynamic_reply_data_cb, dreply, dynamic_reply_free_cb);
283 
284  const WebReply::HeaderMap & headers = dreply->headers();
285  WebReply::HeaderMap::const_iterator i;
286  for (i = headers.begin(); i != headers.end(); ++i) {
287  MHD_add_response_header(response, i->first.c_str(), i->second.c_str());
288  }
289 
290  MHD_RESULT ret = MHD_queue_response(connection, dreply->code(), response);
291  MHD_destroy_response(response);
292 
293  return ret;
294 }
295 
296 /** Queue a static web reply.
297  * @param connection libmicrohttpd connection to queue response to
298  * @param request request this reply is associated to
299  * @param sreply static web reply to queue
300  * @return suitable libmicrohttpd return code
301  */
302 MHD_RESULT
303 WebRequestDispatcher::queue_static_reply(struct MHD_Connection *connection,
304  WebRequest * request,
305  StaticWebReply * sreply)
306 {
307  sreply->set_request(request);
308 
309  struct MHD_Response *response = prepare_static_response(sreply);
310 
311  MHD_RESULT rv = MHD_queue_response(connection, sreply->code(), response);
312  MHD_destroy_response(response);
313  return rv;
314 }
315 
316 /** Queue a static web reply after basic authentication failure.
317  * @param connection libmicrohttpd connection to queue response to
318  * @return suitable libmicrohttpd return code
319  */
320 MHD_RESULT
321 WebRequestDispatcher::queue_basic_auth_fail(struct MHD_Connection *connection, WebRequest *request)
322 {
323  StaticWebReply sreply(WebReply::HTTP_UNAUTHORIZED, UNAUTHORIZED_REPLY);
324 #if MHD_VERSION >= 0x00090400
325  sreply.set_request(request);
326  sreply.pack_caching();
327  sreply.pack();
328  struct MHD_Response *response = prepare_static_response(&sreply);
329 
330  MHD_RESULT rv =
331  static_cast<MHD_RESULT>(MHD_queue_basic_auth_fail_response(connection, realm_, response));
332  MHD_destroy_response(response);
333 #else
334  sreply.add_header(MHD_HTTP_HEADER_WWW_AUTHENTICATE,
335  (std::string("Basic realm=") + realm_).c_str());
336 
337  MHD_RESULT rv = queue_static_reply(connection, request, &sreply);
338 #endif
339  return rv;
340 }
341 
342 /// @cond INTERNALS
343 /** Iterator over key-value pairs where the value
344  * maybe made available in increments and/or may
345  * not be zero-terminated. Used for processing
346  * POST data.
347  *
348  * @param cls user-specified closure
349  * @param kind type of the value
350  * @param key 0-terminated key for the value
351  * @param filename name of the uploaded file, NULL if not known
352  * @param content_type mime-type of the data, NULL if not known
353  * @param transfer_encoding encoding of the data, NULL if not known
354  * @param data pointer to size bytes of data at the
355  * specified offset
356  * @param off offset of data in the overall value
357  * @param size number of bytes in data available
358  * @return MHD_YES to continue iterating,
359  * MHD_NO to abort the iteration
360  */
361 static MHD_RESULT
362 post_iterator(void * cls,
363  enum MHD_ValueKind kind,
364  const char * key,
365  const char * filename,
366  const char * content_type,
367  const char * transfer_encoding,
368  const char * data,
369  uint64_t off,
370  size_t size)
371 {
372  WebRequest *request = static_cast<WebRequest *>(cls);
373 
374  // Cannot handle files, yet
375  if (filename)
376  return MHD_NO;
377 
378  request->set_post_value(key, data + off, size);
379 
380  return MHD_YES;
381 }
382 /// @endcond
383 
384 /** URI logging callback.
385  * @param uri requested URI
386  */
387 void *
388 WebRequestDispatcher::log_uri(const char *uri)
389 {
390  return new WebRequest(uri);
391 }
392 
393 /** Process request callback for libmicrohttpd.
394  * @param connection libmicrohttpd connection instance
395  * @param url URL, may contain escape sequences
396  * @param method HTTP method
397  * @param version HTTP version
398  * @param upload_data uploaded data
399  * @param upload_data_size size of upload_data parameter
400  * @param session_data session data pointer
401  * @return appropriate return code for libmicrohttpd
402  */
403 MHD_RESULT
404 WebRequestDispatcher::process_request(struct MHD_Connection *connection,
405  const char * url,
406  const char * method,
407  const char * version,
408  const char * upload_data,
409  size_t * upload_data_size,
410  void ** session_data)
411 {
412  WebRequest *request = static_cast<WebRequest *>(*session_data);
413 
414  if (!request->is_setup()) {
415  // The first time only the headers are valid,
416  // do not respond in the first round...
417  request->setup(url, method, version, connection);
418 
419  active_requests_mutex_->lock();
420  active_requests_ += 1;
421  active_requests_mutex_->unlock();
422 
423  if (0 == strcmp(method, MHD_HTTP_METHOD_POST)) {
424  request->pp_ = MHD_create_post_processor(connection, 1024, &post_iterator, request);
425  }
426 
427  return MHD_YES;
428  }
429 
430 #if MHD_VERSION >= 0x00090400
431  if (realm_) {
432  char *user, *pass = NULL;
433  user = MHD_basic_auth_get_username_password(connection, &pass);
434  if ((user == NULL) || (pass == NULL) || !user_verifier_->verify_user(user, pass)) {
435  return queue_basic_auth_fail(connection, request);
436  }
437  request->user_ = user;
438  }
439 #endif
440 
441  if (0 == strcmp(method, MHD_HTTP_METHOD_OPTIONS)) {
442  StaticWebReply *reply = new StaticWebReply(WebReply::HTTP_OK);
443  reply->set_caching(true); // handled via Max-Age header anyway
444  const std::map<std::string, std::string> &headers{request->headers()};
445  const auto &request_method = headers.find("Access-Control-Request-Method");
446  const auto &request_headers = headers.find("Access-Control-Request-Headers");
447  if (cors_allow_all_) {
448  reply->add_header("Access-Control-Allow-Origin", "*");
449  if (cors_max_age_ > 0) {
450  reply->add_header("Access-Control-Max-Age", std::to_string(cors_max_age_));
451  }
452  if (request_method != headers.end()) {
453  reply->add_header("Access-Control-Allow-Methods", request_method->second);
454  }
455  if (request_headers != headers.end()) {
456  reply->add_header("Access-Control-Allow-Headers", request_headers->second);
457  }
458  } else if (!cors_origins_.empty()) {
459  const auto &origin = headers.find("Origin");
460  if (origin != headers.end()) {
461  if (std::find(cors_origins_.begin(), cors_origins_.end(), origin->second)
462  != cors_origins_.end()) {
463  reply->add_header("Access-Control-Allow-Origin", origin->second);
464  if (cors_max_age_ > 0) {
465  reply->add_header("Access-Control-Max-Age", std::to_string(cors_max_age_));
466  }
467  if (request_method != headers.end()) {
468  reply->add_header("Access-Control-Allow-Methods", request_method->second);
469  }
470  if (request_headers != headers.end()) {
471  reply->add_header("Access-Control-Allow-Headers", request_headers->second);
472  }
473  } else {
474  reply->set_code(WebReply::HTTP_FORBIDDEN);
475  }
476  } else {
477  reply->set_code(WebReply::HTTP_FORBIDDEN);
478  }
479  }
480  return queue_static_reply(connection, request, reply);
481  delete reply;
482  }
483 
484  if (0 == strcmp(method, MHD_HTTP_METHOD_POST)) {
485  if (MHD_post_process(request->pp_, upload_data, *upload_data_size) == MHD_NO) {
486  request->addto_body(upload_data, *upload_data_size);
487  }
488  if (0 != *upload_data_size) {
489  *upload_data_size = 0;
490  return MHD_YES;
491  }
492  MHD_destroy_post_processor(request->pp_);
493  request->pp_ = NULL;
494  } else if (0 != *upload_data_size) {
495  request->addto_body(upload_data, *upload_data_size);
496  *upload_data_size = 0;
497  return MHD_YES;
498  } else {
499  request->finish_body();
500  }
501 
502  try {
503  WebReply * reply = url_manager_->process_request(request);
504  MHD_RESULT ret;
505 
506  if (reply) {
507  if (cors_allow_all_) {
508  reply->add_header("Access-Control-Allow-Origin", "*");
509  }
510 
511  StaticWebReply * sreply = dynamic_cast<StaticWebReply *>(reply);
512  DynamicWebReply *dreply = dynamic_cast<DynamicWebReply *>(reply);
513  if (sreply) {
514  ret = queue_static_reply(connection, request, sreply);
515  delete reply;
516  } else if (dreply) {
517  ret = queue_dynamic_reply(connection, request, dreply);
518  } else {
519  WebErrorPageReply ereply(WebReply::HTTP_INTERNAL_SERVER_ERROR, "Unknown reply type");
520  ret = queue_static_reply(connection, request, &ereply);
521  delete reply;
522  }
523  } else {
524  WebErrorPageReply ereply(WebReply::HTTP_NOT_FOUND);
525  ret = queue_static_reply(connection, request, &ereply);
526  }
527  return ret;
528  } catch (Exception &e) {
529  WebErrorPageReply ereply(WebReply::HTTP_INTERNAL_SERVER_ERROR, "%s", e.what_no_backtrace());
530  return queue_static_reply(connection, request, &ereply);
531  } catch (std::exception &e) {
532  WebErrorPageReply ereply(WebReply::HTTP_INTERNAL_SERVER_ERROR, "%s", e.what());
533  return queue_static_reply(connection, request, &ereply);
534  }
535 }
536 
537 void
538 WebRequestDispatcher::request_completed(WebRequest *request, MHD_RequestTerminationCode term_code)
539 {
540  active_requests_mutex_->lock();
541  if (active_requests_ > 0)
542  active_requests_ -= 1;
543  last_request_completion_time_->stamp();
544  active_requests_mutex_->unlock();
545  if (access_log_)
546  access_log_->log(request);
547 }
548 
549 /** Get number of active requests.
550  * @return number of ongoing requests.
551  */
552 unsigned int
554 {
555  MutexLocker lock(active_requests_mutex_);
556  return active_requests_;
557 }
558 
559 /** Get time when last request was completed.
560  * @return Time when last request was completed
561  */
562 Time
564 {
565  MutexLocker lock(active_requests_mutex_);
566  return *last_request_completion_time_;
567 }
568 
569 } // end namespace fawkes
Dynamic web reply.
Definition: reply.h:126
virtual size_t next_chunk(size_t pos, char *buffer, size_t buf_max_size)=0
Get data of next chunk.
Base class for exceptions in Fawkes.
Definition: exception.h:36
Mutex locking helper.
Definition: mutex_locker.h:34
Mutex mutual exclusion lock.
Definition: mutex.h:33
void lock()
Lock this mutex.
Definition: mutex.cpp:87
void unlock()
Unlock the mutex.
Definition: mutex.cpp:131
A class for handling time.
Definition: time.h:93
Time & stamp()
Set this time to the current time.
Definition: time.cpp:704
Interface for HTML footer generator.
Interface for HTML header generator.
std::map< std::string, std::string > HeaderMap
Map of headers.
Definition: reply.h:98
@ HTTP_UNAUTHORIZED
UNAUTHORIZED.
Definition: reply.h:61
@ HTTP_OK
OK.
Definition: reply.h:42
@ HTTP_INTERNAL_SERVER_ERROR
INTERNAL_SERVER_ERROR.
Definition: reply.h:85
@ HTTP_FORBIDDEN
FORBIDDEN.
Definition: reply.h:63
@ HTTP_NOT_FOUND
NOT_FOUND.
Definition: reply.h:64
WebRequest * get_request() const
Get associated request.
Definition: reply.cpp:163
void pack_caching()
Called just before the reply is sent.
Definition: reply.cpp:181
Web request dispatcher.
unsigned int active_requests() const
Get number of active requests.
void setup_cors(bool allow_all, std::vector< std::string > &&origins, unsigned int max_age)
Setup cross-origin resource sharing.
void setup_basic_auth(const char *realm, WebUserVerifier *verifier)
Setup basic authentication.
static void * uri_log_cb(void *cls, const char *uri)
Callback for new requests.
static void request_completed_cb(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe)
Process request completion.
static MHD_RESULT process_request_cb(void *callback_data, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **session_data)
Process request callback for libmicrohttpd.
void setup_access_log(const char *filename)
Setup access log.
Time last_request_completion_time() const
Get time when last request was completed.
WebRequestDispatcher(WebUrlManager *url_manager, WebPageHeaderGenerator *headergen=0, WebPageFooterGenerator *footergen=0)
Constructor.
Web request meta data carrier.
Definition: request.h:42
void increment_reply_size(size_t increment_by)
Increment reply bytes counter.
Definition: request.cpp:232
Manage URL mappings.
Definition: url_manager.h:40
Interface for user verification.
Definition: user_verifier.h:29
virtual bool verify_user(const char *user, const char *password)=0
Verify a user.
Webview access_log writer.
Definition: access_log.h:33
void log(const WebRequest *request)
Log a request.
Definition: access_log.cpp:65
Fawkes library namespace.
static void dynamic_reply_free_cb(void *reply)
Callback to free dynamic web reply.
static ssize_t dynamic_reply_data_cb(void *reply, uint64_t pos, char *buf, size_t max)
Callback based chunk-wise data.