paivana-httpd.c (17256B)
1 /* 2 This file is part of GNU Taler 3 Copyright (C) 2012-2014 GNUnet e.V. 4 Copyright (C) 2018, 2025 Taler Systems SA 5 6 GNU Taler is free software; you can redistribute it and/or 7 modify it under the terms of the GNU General Public License 8 as published by the Free Software Foundation; either version 9 3, or (at your option) any later version. 10 11 GNU Taler is distributed in the hope that it will be useful, but 12 WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public 17 License along with GNU Taler; see the file COPYING. If not, 18 write to the Free Software Foundation, Inc., 51 Franklin 19 Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 */ 21 22 /** 23 * @author Martin Schanzenbach 24 * @author Christian Grothoff 25 * @author Marcello Stanisci 26 * @file src/backend/paivana-httpd.c 27 * @brief HTTP proxy that acts as a GNU Taler paywall 28 */ 29 #include "platform.h" 30 #include <curl/curl.h> 31 #include <gnunet/gnunet_util_lib.h> 32 #include <taler/taler_mhd_lib.h> 33 #include "paivana-httpd.h" 34 #include "paivana-httpd_reverse.h" 35 #include "paivana_pd.h" 36 37 38 /* *********************** Globals **************************** */ 39 40 struct RequestContext 41 { 42 /** 43 * Handle for request forwarding as reverse proxy. 44 */ 45 struct HttpRequest *hr; 46 47 /** 48 * We are past the paywall, forward to client. 49 */ 50 bool do_forward; 51 }; 52 53 54 /** 55 * Set to true if we started a daemon. 56 */ 57 static bool have_daemons; 58 59 /** 60 * Static paywall response. 61 */ 62 static struct MHD_Response *paywall; 63 64 /** 65 * Our configuration. 66 */ 67 static const struct GNUNET_CONFIGURATION_Handle *cfg; 68 69 /** 70 * Disable paywall check. 71 */ 72 static int no_check; 73 74 /** 75 * Value to return from main() 76 */ 77 static int global_ret; 78 79 char *target_server_base_url; 80 81 /** 82 * Merchant backend base URL. 83 */ 84 static char *merchant_base_url; 85 86 /** 87 * Merchant backend access token. 88 */ 89 static char *merchant_access_token; 90 91 /** 92 * Secret for the cookie generation. 93 */ 94 static struct GNUNET_HashCode paivana_secret; 95 96 97 /* ********************* Paivana Cookie handling ****************** */ 98 99 /** 100 * Compute access cookie hash for the given @a expiration and @a ca. 101 * 102 * @param expiration expiration time of the cookie 103 * @param ca_len number of bytes in @a ca 104 * @param ca client address 105 * @param[out] c set to the cookie hash 106 */ 107 static void 108 compute_cookie_hash (struct GNUNET_TIME_Timestamp expiration, 109 size_t ca_len, 110 const void *ca, 111 struct GNUNET_HashCode *c) 112 { 113 struct GNUNET_TIME_AbsoluteNBO e; 114 115 e = GNUNET_TIME_absolute_hton (expiration.abs_time); 116 GNUNET_assert (GNUNET_YES == 117 GNUNET_CRYPTO_hkdf_gnunet ( 118 c, /* result */ 119 sizeof (c), 120 &e, /* salt */ 121 sizeof (e), 122 &paivana_secret, /* source key material */ 123 sizeof (paivana_secret), 124 GNUNET_CRYPTO_kdf_arg (ca, 125 ca_len))); 126 } 127 128 129 /** 130 * Check if the given cookie currently grants access. 131 * 132 * @param cookie the cookie 133 * @param ca_len number of bytes in @a ca 134 * @param ca client address 135 * @return true if the cookie is OK 136 */ 137 static bool 138 check_cookie (const char *cookie, 139 size_t ca_len, 140 const void *ca) 141 { 142 const char *dash; 143 unsigned long long u; 144 struct GNUNET_HashCode h; 145 struct GNUNET_HashCode c; 146 struct GNUNET_TIME_Timestamp a; 147 148 dash = strchr (cookie, 149 '-'); 150 if (NULL == dash) 151 return false; 152 dash++; 153 if (1 != 154 sscanf (cookie, 155 "%llu-", 156 &u)) 157 return false; 158 a.abs_time.abs_value_us = u * 1000LLU * 1000LLU; 159 if (GNUNET_TIME_absolute_is_past (a.abs_time)) 160 return false; 161 if (GNUNET_OK != 162 GNUNET_STRINGS_string_to_data (dash, 163 strlen (dash), 164 &c, 165 sizeof (c))) 166 return false; 167 compute_cookie_hash (a, 168 ca_len, 169 ca, 170 &h); 171 return (0 == 172 GNUNET_memcmp (&c, 173 &h)); 174 } 175 176 177 /** 178 * Compute access cookie hash for the given @a expiration and @a ca. 179 * 180 * @param expiration expiration time of the cookie 181 * @param ca_len number of bytes in @a ca 182 * @param ca client address 183 * @param[out] c set to the cookie hash 184 */ 185 static char * 186 compute_cookie (struct GNUNET_TIME_Timestamp expiration, 187 size_t ca_len, 188 const void *ca) 189 { 190 struct GNUNET_HashCode h; 191 char *end; 192 char cstr[128]; 193 char *res; 194 195 compute_cookie_hash (expiration, 196 ca_len, 197 ca, 198 &h); 199 end = GNUNET_STRINGS_data_to_string (&h, 200 sizeof (h), 201 cstr, 202 sizeof (cstr)); 203 *end = '\0'; 204 GNUNET_asprintf ( 205 &res, 206 "%llu-%s", 207 (unsigned long long) (expiration.abs_time.abs_value_us / 1000LLU / 1000LLU), 208 cstr); 209 return res; 210 } 211 212 213 /* *************** MHD response generation ***************** */ 214 215 216 /** 217 * Main MHD callback for handling requests. 218 * 219 * @param cls unused 220 * @param con MHD connection handle 221 * @param url the url in the request 222 * @param meth the HTTP method used ("GET", "PUT", etc.) 223 * @param ver the HTTP version string (i.e. "HTTP/1.1") 224 * @param upload_data the data being uploaded (excluding HEADERS, 225 * for a POST that fits into memory and that is encoded 226 * with a supported encoding, the POST data will NOT be 227 * given in upload_data and is instead available as 228 * part of MHD_get_connection_values; very large POST 229 * data *will* be made available incrementally in 230 * upload_data) 231 * @param upload_data_size set initially to the size of the 232 * @a upload_data provided; the method must update this 233 * value to the number of bytes NOT processed; 234 * @param con_cls pointer to location where we store the 235 * 'struct Request' 236 * @return #MHD_YES if the connection was handled successfully, 237 * #MHD_NO if the socket must be closed due to a serious 238 * error while handling the request 239 */ 240 static enum MHD_Result 241 create_response (void *cls, 242 struct MHD_Connection *con, 243 const char *url, 244 const char *meth, 245 const char *ver, 246 const char *upload_data, 247 size_t *upload_data_size, 248 void **con_cls) 249 { 250 struct RequestContext *rc = *con_cls; 251 struct HttpRequest *hr = rc->hr; 252 253 (void) cls; 254 if (NULL == hr) 255 { 256 GNUNET_break (0); 257 return MHD_NO; 258 } 259 // FIXME: check if url is one that we reverse proxy! 260 261 if (! rc->do_forward) 262 { 263 const char *cookie; 264 bool ok = (0 != no_check); 265 266 cookie = MHD_lookup_connection_value (con, 267 MHD_COOKIE_KIND, 268 "Paivana-Cookie"); 269 if (NULL != cookie) 270 { 271 const union MHD_ConnectionInfo *ci; 272 const struct sockaddr *ca; 273 socklen_t ca_len; 274 275 ci = MHD_get_connection_info (con, 276 MHD_CONNECTION_INFO_CLIENT_ADDRESS); 277 GNUNET_assert (NULL != ci); 278 ca = ci->client_addr; 279 switch (ca->sa_family) 280 { 281 case AF_INET: 282 ca_len = sizeof (struct sockaddr_in); 283 break; 284 case AF_INET6: 285 ca_len = sizeof (struct sockaddr_in6); 286 break; 287 default: 288 GNUNET_break (0); 289 ca_len = 0; 290 break; 291 } 292 ok = check_cookie (cookie, 293 ca_len, 294 ca); 295 } 296 if (! ok) 297 { 298 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 299 "Request denied\n"); 300 return MHD_queue_response (con, 301 MHD_HTTP_PAYMENT_REQUIRED, 302 paywall); 303 } 304 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 305 "Request ok!\n"); 306 rc->do_forward = true; 307 /* TODO: hacks for 100 continue suppression should go here! */ 308 return MHD_YES; 309 } 310 311 return PAIVANA_HTTPD_reverse (hr, 312 con, 313 url, 314 meth, 315 ver, 316 upload_data, 317 upload_data_size); 318 } 319 320 321 /* ************ MHD HTTP setup and event loop *************** */ 322 323 324 /** 325 * Function called when MHD decides that we 326 * are done with a request. 327 * 328 * @param cls NULL 329 * @param connection connection handle 330 * @param con_cls value as set by the last call to 331 * the MHD_AccessHandlerCallback, should be 332 * our `struct RequestContext *` (created in `mhd_log_callback()`) 333 * @param toe reason for request termination (ignored) 334 */ 335 static void 336 mhd_completed_cb (void *cls, 337 struct MHD_Connection *connection, 338 void **con_cls, 339 enum MHD_RequestTerminationCode toe) 340 { 341 struct RequestContext *rc = *con_cls; 342 343 (void) cls; 344 (void) connection; 345 if (NULL == rc) 346 return; 347 if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe) 348 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 349 "MHD encountered error handling request: %d\n", 350 toe); 351 PAIVANA_HTTPD_reverse_cleanup (rc->hr); 352 GNUNET_free (rc); 353 *con_cls = NULL; 354 } 355 356 357 /** 358 * Function called when MHD first processes an incoming connection. 359 * Gives us the respective URI information. 360 * 361 * We use this to associate the `struct MHD_Connection` with our 362 * internal `struct HttpRequest` data structure (by checking 363 * for matching sockets). 364 * 365 * @param cls the HTTP server handle (a `struct MhdHttpList`) 366 * @param url the URL that is being requested 367 * @param connection MHD connection object for the request 368 * @return the `struct RequestContext` that this @a connection is for 369 */ 370 static void * 371 mhd_log_callback (void *cls, 372 const char *url, 373 struct MHD_Connection *connection) 374 { 375 struct RequestContext *rc; 376 const union MHD_ConnectionInfo *ci; 377 378 (void) cls; 379 ci = MHD_get_connection_info (connection, 380 MHD_CONNECTION_INFO_SOCKET_CONTEXT); 381 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 382 "Processing %s\n", 383 url); 384 if (NULL == ci) 385 { 386 GNUNET_break (0); 387 return NULL; 388 } 389 rc = GNUNET_new (struct RequestContext); 390 rc->hr = PAIVANA_HTTPD_reverse_create (connection, 391 url); 392 return rc; 393 } 394 395 396 /* *************** General / main code *************** */ 397 398 399 /** 400 * Task run on shutdown 401 * 402 * @param cls closure 403 */ 404 static void 405 do_shutdown (void *cls) 406 { 407 (void) cls; 408 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 409 "Shutting down...\n"); 410 TALER_MHD_daemons_halt (); 411 PAIVANA_HTTPD_reverse_shutdown (); 412 TALER_MHD_daemons_destroy (); 413 GNUNET_free (target_server_base_url); 414 } 415 416 417 /** 418 * Try to initialize the paywall response. 419 */ 420 static bool 421 load_paywall (void) 422 { 423 char *tpath; 424 char *fn; 425 int fd; 426 struct stat sb; 427 428 tpath = GNUNET_OS_installation_get_path (PAIVANA_project_data (), 429 GNUNET_OS_IPK_DATADIR); 430 GNUNET_asprintf (&fn, 431 "%s/paywall.html", 432 tpath); 433 GNUNET_free (tpath); 434 fd = open (fn, 435 O_RDONLY); 436 if (-1 == fd) 437 { 438 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 439 "open", 440 fn); 441 GNUNET_free (fn); 442 return false; 443 } 444 if (0 != 445 fstat (fd, 446 &sb)) 447 { 448 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 449 "stat", 450 fn); 451 GNUNET_free (fn); 452 GNUNET_break (0 == close (fd)); 453 return false; 454 } 455 GNUNET_free (fn); 456 paywall = MHD_create_response_from_fd (sb.st_size, 457 fd); 458 if (NULL == paywall) 459 return false; 460 GNUNET_break (MHD_YES == 461 MHD_add_response_header (paywall, 462 MHD_HTTP_HEADER_CONTENT_TYPE, 463 "text/html")); 464 return true; 465 } 466 467 468 /** 469 * Callback invoked on every listen socket to start the 470 * respective MHD HTTP daemon. 471 * 472 * @param cls unused 473 * @param lsock the listen socket 474 */ 475 static void 476 start_daemon (void *cls, 477 int lsock) 478 { 479 struct MHD_Daemon *mhd; 480 481 (void) cls; 482 GNUNET_assert (-1 != lsock); 483 mhd = MHD_start_daemon ( 484 MHD_USE_DEBUG 485 | MHD_ALLOW_SUSPEND_RESUME 486 | MHD_USE_DUAL_STACK, 487 0, 488 NULL, NULL, 489 &create_response, NULL, 490 MHD_OPTION_LISTEN_SOCKET, 491 lsock, 492 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 16, 493 MHD_OPTION_NOTIFY_COMPLETED, &mhd_completed_cb, NULL, 494 MHD_OPTION_URI_LOG_CALLBACK, &mhd_log_callback, NULL, 495 MHD_OPTION_END); 496 497 if (NULL == mhd) 498 { 499 GNUNET_break (0); 500 GNUNET_SCHEDULER_shutdown (); 501 return; 502 } 503 have_daemons = true; 504 TALER_MHD_daemon_start (mhd); 505 } 506 507 508 /** 509 * Main function that will be run. Main tasks are (1) init. the 510 * curl infrastructure (curl_global_init() / curl_multi_init()), 511 * then fetch the HTTP port where its Web service should listen at, 512 * and finally start MHD on that port. 513 * 514 * @param cls closure 515 * @param args remaining command-line arguments 516 * @param cfgfile name of the configuration file used (for saving, can be NULL!) 517 * @param c configuration 518 */ 519 static void 520 run (void *cls, 521 char *const *args, 522 const char *cfgfile, 523 const struct GNUNET_CONFIGURATION_Handle *c) 524 { 525 enum GNUNET_GenericReturnValue ret; 526 char *secret; 527 528 (void) cls; 529 (void) args; 530 (void) cfgfile; 531 cfg = c; 532 533 if (! load_paywall ()) 534 { 535 GNUNET_SCHEDULER_shutdown (); 536 return; 537 } 538 if (! PAIVANA_HTTPD_reverse_init ()) 539 { 540 GNUNET_SCHEDULER_shutdown (); 541 return; 542 } 543 544 /* No need to check return value. If given, we take, 545 * otherwise it stays zero. */ 546 if (GNUNET_OK != 547 GNUNET_CONFIGURATION_get_value_string ( 548 c, 549 "paivana", 550 "DESTINATION_BASE_URL", 551 &target_server_base_url)) 552 { 553 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 554 "paivana", 555 "DESTINATION_BASE_URL"); 556 GNUNET_SCHEDULER_shutdown (); 557 return; 558 } 559 if (GNUNET_OK != 560 GNUNET_CONFIGURATION_get_value_string ( 561 c, 562 "paivana", 563 "MERCHANT_BACKEND_URL", 564 &merchant_base_url)) 565 { 566 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 567 "paivana", 568 "MERCHANT_BACKEND_URL"); 569 GNUNET_SCHEDULER_shutdown (); 570 return; 571 } 572 if (GNUNET_OK != 573 GNUNET_CONFIGURATION_get_value_string ( 574 c, 575 "paivana", 576 "MERCHANT_ACCESS_TOKEN", 577 &merchant_access_token)) 578 { 579 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 580 "paivana", 581 "MERCHANT_ACCESS_TOKEN"); 582 GNUNET_SCHEDULER_shutdown (); 583 return; 584 } 585 if (GNUNET_OK != 586 GNUNET_CONFIGURATION_get_value_string ( 587 c, 588 "paivana", 589 "SECRET", 590 &secret)) 591 { 592 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, 593 "paivana", 594 "SECRET"); 595 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, 596 &paivana_secret, 597 sizeof (paivana_secret)); 598 } 599 else 600 { 601 GNUNET_CRYPTO_hash (secret, 602 strlen (secret), 603 &paivana_secret); 604 GNUNET_free (secret); 605 } 606 GNUNET_SCHEDULER_add_shutdown (&do_shutdown, 607 NULL); 608 609 ret = TALER_MHD_listen_bind (c, 610 "paivana", 611 &start_daemon, 612 NULL); 613 switch (ret) 614 { 615 case GNUNET_SYSERR: 616 global_ret = EXIT_NOTCONFIGURED; 617 GNUNET_SCHEDULER_shutdown (); 618 return; 619 case GNUNET_NO: 620 if (! have_daemons) 621 { 622 global_ret = EXIT_NOTCONFIGURED; 623 GNUNET_SCHEDULER_shutdown (); 624 return; 625 } 626 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 627 "Could not open all configured listen sockets\n"); 628 break; 629 case GNUNET_OK: 630 break; 631 } 632 } 633 634 635 /** 636 * Main function. 637 */ 638 int 639 main (int argc, 640 char *const *argv) 641 { 642 struct GNUNET_GETOPT_CommandLineOption options[] = { 643 GNUNET_GETOPT_option_flag ( 644 'n', 645 "no-payment", 646 gettext_noop ( 647 "disables payment, useful for testing reverse-proxy only"), 648 &no_check), 649 GNUNET_GETOPT_OPTION_END 650 }; 651 enum GNUNET_GenericReturnValue ret; 652 653 ret = GNUNET_PROGRAM_run ( 654 PAIVANA_project_data (), 655 argc, 656 argv, 657 "paivana-httpd", 658 "reverse proxy requesting Taler payment", 659 options, 660 &run, NULL); 661 if (GNUNET_SYSERR == ret) 662 return EXIT_INVALIDARGUMENT; 663 if (GNUNET_NO == ret) 664 return EXIT_SUCCESS; 665 return global_ret; 666 } 667 668 669 /* end of paivana-httpd.c */