paivana

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

paivana-httpd_daemon.c (11271B)


      1 /*
      2      This file is part of GNUnet.
      3      Copyright (C) 2026 Taler Systems SA
      4 
      5      Paivana is free software; you can redistribute it and/or
      6      modify it under the terms of the GNU General Public License
      7      as published by the Free Software Foundation; either version
      8      3, or (at your option) any later version.
      9 
     10      Paivana is distributed in the hope that it will be useful,
     11      but WITHOUT ANY WARRANTY; without even the implied warranty
     12      of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
     13      the GNU General Public License for more details.
     14 
     15      You should have received a copy of the GNU General Public
     16      License along with Paivana; see the file COPYING.  If not,
     17      write to the Free Software Foundation, Inc., 51 Franklin
     18      Street, Fifth Floor, Boston, MA 02110-1301, USA.
     19 */
     20 
     21 /**
     22  * @author Christian Grothoff
     23  * @file paivana-httpd_daemon.c
     24  * @brief daemon functions
     25  */
     26 
     27 #include "platform.h"
     28 #include <curl/curl.h>
     29 #include <gnunet/gnunet_util_lib.h>
     30 #include <gnunet/gnunet_curl_lib.h>
     31 #include <taler/taler_mhd_lib.h>
     32 #include "paivana-httpd_cookie.h"
     33 #include "paivana-httpd_daemon.h"
     34 #include "paivana-httpd_helper.h"
     35 #include "paivana-httpd_pay.h"
     36 #include "paivana-httpd_reverse.h"
     37 #include "paivana-httpd_templates.h"
     38 
     39 
     40 struct RequestContext
     41 {
     42 
     43   /**
     44    * HTTP connection to the client.
     45    */
     46   struct MHD_Connection *connection;
     47 
     48   /**
     49    * Handle for request forwarding as reverse proxy.
     50    */
     51   struct HttpRequest *hr;
     52 
     53   /**
     54    * Handle for processing actual payment.
     55    */
     56   struct PayRequest *hp;
     57 
     58   /**
     59    * Full request URL.
     60    */
     61   char *url;
     62 
     63   /**
     64    * True if this is a POST to the .well-known/paivana endpoint.
     65    */
     66   bool is_paivana;
     67 
     68   /**
     69    * We are past the paywall, forward to client.
     70    */
     71   bool do_forward;
     72 };
     73 
     74 
     75 /**
     76  * Set to true if we started a daemon.
     77  */
     78 static bool have_daemons;
     79 
     80 
     81 /**
     82  * Main MHD callback for handling requests.
     83  *
     84  * @param cls unused
     85  * @param con MHD connection handle
     86  * @param url the url in the request
     87  * @param meth the HTTP method used ("GET", "PUT", etc.)
     88  * @param ver the HTTP version string (i.e. "HTTP/1.1")
     89  * @param upload_data the data being uploaded (excluding HEADERS,
     90  *        for a POST that fits into memory and that is encoded
     91  *        with a supported encoding, the POST data will NOT be
     92  *        given in upload_data and is instead available as
     93  *        part of MHD_get_connection_values; very large POST
     94  *        data *will* be made available incrementally in
     95  *        upload_data)
     96  * @param upload_data_size set initially to the size of the
     97  *        @a upload_data provided; the method must update this
     98  *        value to the number of bytes NOT processed;
     99  * @param con_cls pointer to location where we store the
    100  *        'struct Request'
    101  * @return #MHD_YES if the connection was handled successfully,
    102  *         #MHD_NO if the socket must be closed due to a serious
    103  *         error while handling the request
    104  */
    105 static enum MHD_Result
    106 create_response (void *cls,
    107                  struct MHD_Connection *con,
    108                  const char *url,
    109                  const char *meth,
    110                  const char *ver,
    111                  const char *upload_data,
    112                  size_t *upload_data_size,
    113                  void **con_cls)
    114 {
    115   struct RequestContext *rc = *con_cls;
    116   const char *cookie;
    117   bool ok = false;
    118   struct GNUNET_Buffer buf;
    119   char *website;
    120 
    121   (void) cls;
    122   memset (&buf,
    123           0,
    124           sizeof (buf));
    125   if ( (! rc->is_paivana) &&
    126        (0 == strcmp (url,
    127                      "/.well-known/paivana")) &&
    128        (0 == strcasecmp (meth,
    129                          MHD_HTTP_METHOD_POST)) )
    130   {
    131     rc->is_paivana = true;
    132   }
    133   if (rc->is_paivana)
    134   {
    135     if (PH_no_check)
    136     {
    137       /* paywall disabled, 501 */
    138       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    139                   "Paywall disabled, refusing to respond to %s\n",
    140                   url);
    141       return TALER_MHD_reply_with_error (rc->connection,
    142                                          MHD_HTTP_NOT_IMPLEMENTED,
    143                                          TALER_EC_PAIVANA_PAYWALL_DISABLED,
    144                                          NULL);
    145     }
    146     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    147                 "Client POSTed payment, checking validity\n");
    148     if (NULL == rc->hp)
    149       rc->hp = PAIVANA_HTTPD_payment_create (rc->connection);
    150     return PAIVANA_HTTPD_payment_handle (rc->hp,
    151                                          upload_data,
    152                                          upload_data_size);
    153   }
    154 
    155   if (PH_have_whitelist_ex && (! rc->do_forward))
    156   {
    157     rc->do_forward = (0 ==
    158                       regexec (&PH_whitelist_ex,
    159                                url,
    160                                0,
    161                                NULL,
    162                                0));
    163   }
    164 
    165   if (rc->do_forward)
    166     goto do_forward;
    167 
    168   if ( (0 == strncmp (url,
    169                       "/.well-known/paivana/templates/",
    170                       strlen ("/.well-known/paivana/templates/"))) &&
    171        ( (0 == strcasecmp (meth,
    172                            MHD_HTTP_METHOD_GET)) ||
    173          /* HEAD must be answered like GET (MHD suppresses the body);
    174             otherwise it falls through to the paywall check below,
    175             which redirects to this very URL again -- forever. */
    176          (0 == strcasecmp (meth,
    177                            MHD_HTTP_METHOD_HEAD)) ) )
    178   {
    179     const char *id = &url[strlen ("/.well-known/paivana/templates/")];
    180 
    181     return PAIVANA_HTTPD_return_template (rc->connection,
    182                                           id);
    183   }
    184 
    185   if (! PAIVANA_HTTPD_get_base_url (con,
    186                                     &buf))
    187   {
    188     GNUNET_break (0);
    189     return TALER_MHD_reply_with_error (
    190       con,
    191       MHD_HTTP_BAD_REQUEST,
    192       TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
    193       "Host or X-Forwarded-Host required");
    194   }
    195   GNUNET_buffer_write_str (&buf,
    196                            url);
    197   website = GNUNET_buffer_reap_str (&buf);
    198   cookie = MHD_lookup_connection_value (con,
    199                                         MHD_COOKIE_KIND,
    200                                         "Paivana-Cookie");
    201   if (NULL != cookie)
    202   {
    203     void *ca = NULL;
    204     size_t ca_len = 0;
    205 
    206     /* If we cannot get the client address, we just
    207        use 0/NULL and log an error. */
    208     GNUNET_break (PAIVANA_HTTPD_get_client_address (con,
    209                                                     &ca,
    210                                                     &ca_len));
    211     ok = PAIVANA_HTTPD_check_cookie (cookie,
    212                                      website,
    213                                      ca_len,
    214                                      ca);
    215     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    216                 "Client sent cookie %s for %s: %s\n",
    217                 cookie,
    218                 website,
    219                 ok ? "good" : "invalid");
    220     GNUNET_free (ca);
    221   }
    222   if (! ok)
    223   {
    224     enum GNUNET_GenericReturnValue ret;
    225 
    226     ret = PAIVANA_HTTPD_search_templates (con,
    227                                           website);
    228     if (GNUNET_SYSERR != ret)
    229     {
    230       GNUNET_free (website);
    231       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    232                   "Payment required, sending paywall page %s\n",
    233                   (GNUNET_OK == ret) ? "ok" : "failed");
    234       return (GNUNET_OK == ret) ? MHD_YES : MHD_NO;
    235     }
    236   }
    237   GNUNET_free (website);
    238   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    239               "Request OK, no paywall applies!\n");
    240   rc->do_forward = true;
    241 do_forward:
    242   if (NULL == rc->hr)
    243     rc->hr = PAIVANA_HTTPD_reverse_create (rc->connection,
    244                                            rc->url);
    245   return PAIVANA_HTTPD_reverse (rc->hr,
    246                                 con,
    247                                 url,
    248                                 meth,
    249                                 ver,
    250                                 upload_data,
    251                                 upload_data_size);
    252 }
    253 
    254 
    255 /**
    256  * Function called when MHD decides that we
    257  * are done with a request.
    258  *
    259  * @param cls NULL
    260  * @param connection connection handle
    261  * @param con_cls value as set by the last call to
    262  *        the MHD_AccessHandlerCallback, should be
    263  *        our `struct RequestContext *` (created in `mhd_log_callback()`)
    264  * @param toe reason for request termination (ignored)
    265  */
    266 static void
    267 mhd_completed_cb (void *cls,
    268                   struct MHD_Connection *connection,
    269                   void **con_cls,
    270                   enum MHD_RequestTerminationCode toe)
    271 {
    272   struct RequestContext *rc = *con_cls;
    273 
    274   (void) cls;
    275   (void) connection;
    276   if (NULL == rc)
    277     return;
    278   if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe)
    279     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    280                 "MHD encountered error handling request to %s: %d\n",
    281                 rc->url,
    282                 toe);
    283   if (NULL != rc->hr)
    284     PAIVANA_HTTPD_reverse_cleanup (rc->hr);
    285   if (NULL != rc->hp)
    286     PAIVANA_HTTPD_payment_destroy (rc->hp);
    287   GNUNET_free (rc->url);
    288   GNUNET_free (rc);
    289   *con_cls = NULL;
    290 }
    291 
    292 
    293 /**
    294  * Function called when MHD first processes an incoming connection.
    295  * Gives us the respective URI information.
    296  *
    297  * We use this to associate the `struct MHD_Connection` with our
    298  * internal `struct HttpRequest` data structure (by checking
    299  * for matching sockets).
    300  *
    301  * @param cls the HTTP server handle (a `struct MhdHttpList`)
    302  * @param url the URL that is being requested
    303  * @param connection MHD connection object for the request
    304  * @return the `struct RequestContext` that this @a connection is for
    305  */
    306 static void *
    307 mhd_log_callback (void *cls,
    308                   const char *url,
    309                   struct MHD_Connection *connection)
    310 {
    311   struct RequestContext *rc;
    312 
    313   rc = GNUNET_new (struct RequestContext);
    314   rc->connection = connection;
    315   rc->url = GNUNET_strdup (url);
    316   rc->do_forward = (1 == PH_no_check);
    317   return rc;
    318 }
    319 
    320 
    321 /**
    322  * Callback invoked on every listen socket to start the
    323  * respective MHD HTTP daemon.
    324  *
    325  * @param cls unused
    326  * @param lsock the listen socket
    327  */
    328 static void
    329 start_daemon (void *cls,
    330               int lsock)
    331 {
    332   struct MHD_Daemon *mhd;
    333 
    334   (void) cls;
    335   GNUNET_assert (-1 != lsock);
    336   mhd = MHD_start_daemon (
    337     MHD_USE_DEBUG
    338     | MHD_ALLOW_SUSPEND_RESUME
    339     | MHD_USE_DUAL_STACK,
    340     0,
    341     NULL, NULL,
    342     &create_response, NULL,
    343     MHD_OPTION_LISTEN_SOCKET,
    344     lsock,
    345     MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 16,
    346     MHD_OPTION_NOTIFY_COMPLETED, &mhd_completed_cb, NULL,
    347     MHD_OPTION_URI_LOG_CALLBACK, &mhd_log_callback, NULL,
    348     MHD_OPTION_END);
    349 
    350   if (NULL == mhd)
    351   {
    352     GNUNET_break (0);
    353     GNUNET_SCHEDULER_shutdown ();
    354     return;
    355   }
    356   have_daemons = true;
    357   TALER_MHD_daemon_start (mhd);
    358 }
    359 
    360 
    361 void
    362 PAIVANA_HTTPD_serve_requests ()
    363 {
    364   enum GNUNET_GenericReturnValue ret;
    365 
    366   ret = TALER_MHD_listen_bind (PH_cfg,
    367                                "paivana",
    368                                &start_daemon,
    369                                NULL);
    370   switch (ret)
    371   {
    372   case GNUNET_SYSERR:
    373     PH_global_ret = EXIT_NOTCONFIGURED;
    374     GNUNET_SCHEDULER_shutdown ();
    375     return;
    376   case GNUNET_NO:
    377     if (! have_daemons)
    378     {
    379       PH_global_ret = EXIT_NOTCONFIGURED;
    380       GNUNET_SCHEDULER_shutdown ();
    381       return;
    382     }
    383     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    384                 "Could not open all configured listen sockets\n");
    385     break;
    386   case GNUNET_OK:
    387     break;
    388   }
    389 }