paivana

HTTP paywall reverse proxy
Log | Files | Refs | README | LICENSE

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 */