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
51namespace 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 */
99void
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 */
120void
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 */
133void
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 */
148void *
149WebRequestDispatcher::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 */
166MHD_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 */
187void
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 */
207static ssize_t
208dynamic_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 */
221static 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 */
232struct MHD_Response *
233WebRequestDispatcher::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 */
271MHD_RESULT
272WebRequestDispatcher::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 */
302MHD_RESULT
303WebRequestDispatcher::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 */
320MHD_RESULT
321WebRequestDispatcher::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 */
361static MHD_RESULT
362post_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 */
387void *
388WebRequestDispatcher::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 */
403MHD_RESULT
404WebRequestDispatcher::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
537void
538WebRequestDispatcher::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 */
552unsigned 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 */
562Time
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) noexcept=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.