paivana

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

paivana-httpd_templates.c (18349B)


      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_templates.c
     24  * @brief template functions
     25  */
     26 #include "platform.h"
     27 #include <curl/curl.h>
     28 #include <gnunet/gnunet_util_lib.h>
     29 #include <gnunet/gnunet_uri_lib.h>
     30 #include <gnunet/gnunet_curl_lib.h>
     31 #include "paivana-httpd.h"
     32 #include "paivana-httpd_daemon.h"
     33 #include "paivana-httpd_helper.h"
     34 #include "paivana-httpd_templates.h"
     35 #include <taler/taler_mhd_lib.h>
     36 #include <taler/taler_templating_lib.h>
     37 #include "paivana_pd.h"
     38 #include <regex.h>
     39 
     40 
     41 struct Template;
     42 #define TALER_MERCHANT_GET_PRIVATE_TEMPLATE_RESULT_CLOSURE struct Template
     43 #include <taler/merchant/get-private-templates-TEMPLATE_ID.h>
     44 #include <taler/merchant/get-private-templates.h>
     45 
     46 
     47 /**
     48  * Entry in the cache of responses for a given template.
     49  */
     50 struct ResponseCacheEntry
     51 {
     52 
     53   /**
     54    * Kept in a DLL.
     55    */
     56   struct ResponseCacheEntry *next;
     57 
     58   /**
     59    * Kept in a DLL.
     60    */
     61   struct ResponseCacheEntry *prev;
     62 
     63   /**
     64    * Language of the response.
     65    */
     66   char *lang;
     67 
     68   /**
     69    * Accept-Encoding of the response.
     70    */
     71   char *ae;
     72 
     73   /**
     74    * Paywall response for these request parameters.
     75    */
     76   struct MHD_Response *paywall;
     77 
     78   /**
     79    * HTTP status to return with @e paywall.
     80    */
     81   unsigned int http_status;
     82 
     83 };
     84 
     85 
     86 /**
     87  * Information about a template in the merchant backend.
     88  */
     89 struct Template
     90 {
     91 
     92   /**
     93    * Kept in a DLL.
     94    */
     95   struct Template *next;
     96 
     97   /**
     98    * Kept in a DLL.
     99    */
    100   struct Template *prev;
    101 
    102   /**
    103    * ID of the template.
    104    */
    105   char *template_id;
    106 
    107   /**
    108    * Maximum pickup delay for the pages.
    109    */
    110   struct GNUNET_TIME_Relative max_pickup_delay;
    111 
    112   /**
    113    * Regular expression of websites the template is for.
    114    */
    115   char *regex;
    116 
    117   /**
    118    * Pre-compiled regular expression @e regex.
    119    */
    120   regex_t ex;
    121 
    122   /**
    123    * Handle used to request more information about the template.
    124    */
    125   struct TALER_MERCHANT_GetPrivateTemplateHandle *gt;
    126 
    127   /**
    128    * Kept in a DLL.
    129    */
    130   struct ResponseCacheEntry *rce_head;
    131 
    132   /**
    133    * Kept in a DLL.
    134    */
    135   struct ResponseCacheEntry *rce_tail;
    136 
    137 };
    138 
    139 
    140 /**
    141  * Kept in a DLL.
    142  */
    143 static struct Template *t_head;
    144 
    145 /**
    146  * Kept in a DLL.
    147  */
    148 static struct Template *t_tail;
    149 
    150 /**
    151  * Handle to get all the templates.
    152  */
    153 static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt;
    154 
    155 
    156 /**
    157  * Check if two strings are equal, including both being NULL
    158  *
    159  * @param s1 a string, possibly NULL
    160  * @param s2 a string. possibly NULL
    161  * @return true if both are equal
    162  */
    163 static bool
    164 eq (const char *s1,
    165     const char *s2)
    166 {
    167   if (s1 == s2)
    168     return true;
    169   if (NULL == s1)
    170     return false;
    171   if (NULL == s2)
    172     return false;
    173   return (0 == strcmp (s1,
    174                        s2));
    175 }
    176 
    177 
    178 /**
    179  * Create a taler://pay-template/ URI for the given @a con and @a template_id
    180  * and @a instance_id.
    181  *
    182  * @param merchant_base_url URL to take host and path from;
    183  *        we cannot take it from the MHD connection as a browser
    184  *        may have changed 'http' to 'https' and we MUST be consistent
    185  *        with what the merchant's frontend used initially
    186  * @param template_id the template id
    187  * @return corresponding taler://pay-template/ URI, or NULL on missing "host"
    188  */
    189 static char *
    190 make_taler_pay_template_uri (const char *merchant_base_url,
    191                              const char *template_id)
    192 {
    193   struct GNUNET_Buffer buf = { 0 };
    194   char *url;
    195   struct GNUNET_Uri uri;
    196 
    197   url = GNUNET_strdup (merchant_base_url);
    198   if (-1 == GNUNET_uri_parse (&uri,
    199                               url))
    200   {
    201     GNUNET_break (0);
    202     GNUNET_free (url);
    203     return NULL;
    204   }
    205   GNUNET_assert (NULL != template_id);
    206   GNUNET_buffer_write_str (&buf,
    207                            "taler");
    208   if (0 == strcasecmp ("http",
    209                        uri.scheme))
    210     GNUNET_buffer_write_str (&buf,
    211                              "+http");
    212   GNUNET_buffer_write_str (&buf,
    213                            "://pay-template/");
    214   GNUNET_buffer_write_str (&buf,
    215                            uri.host);
    216   if (0 != uri.port)
    217     GNUNET_buffer_write_fstr (&buf,
    218                               ":%u",
    219                               (unsigned int) uri.port);
    220   if (NULL != uri.path)
    221     GNUNET_buffer_write_path (&buf,
    222                               uri.path);
    223   GNUNET_buffer_write_path (&buf,
    224                             template_id);
    225   GNUNET_free (url);
    226   return GNUNET_buffer_reap_str (&buf);
    227 }
    228 
    229 
    230 /**
    231  * Try to initialize the paywall response.
    232  *
    233  * @param conn connection to create the response for
    234  * @param t template template to create the response for
    235  * @return MHD status code to return
    236  */
    237 static enum MHD_Result
    238 load_paywall (struct MHD_Connection *conn,
    239               struct Template *t)
    240 {
    241   struct MHD_Response *reply;
    242   const char *lang;
    243   const char *ae;
    244   unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED;
    245 
    246   lang = MHD_lookup_connection_value (conn,
    247                                       MHD_HEADER_KIND,
    248                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    249   ae = MHD_lookup_connection_value (conn,
    250                                     MHD_HEADER_KIND,
    251                                     MHD_HTTP_HEADER_ACCEPT_ENCODING);
    252   for (struct ResponseCacheEntry *pos = t->rce_head;
    253        NULL != pos;
    254        pos = pos->next)
    255   {
    256     if ( (eq (lang,
    257               pos->lang)) &&
    258          (eq (ae,
    259               pos->ae) ) )
    260       return MHD_queue_response (conn,
    261                                  pos->http_status,
    262                                  pos->paywall);
    263   }
    264 
    265   {
    266     enum GNUNET_GenericReturnValue ret;
    267     json_t *data;
    268 
    269     data = GNUNET_JSON_PACK (
    270       GNUNET_JSON_pack_string (
    271         "template_id",
    272         t->template_id),
    273       GNUNET_JSON_pack_uint64 (
    274         "max_pickup_delay",
    275         t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
    276       GNUNET_JSON_pack_string (
    277         "merchant_backend",
    278         PH_merchant_base_url));
    279     ret = TALER_TEMPLATING_build (
    280       conn,
    281       &http_status,
    282       "paywall",
    283       NULL /* no instance */,
    284       NULL /* no Taler URI (needs dynamic paivana_id!) */,
    285       data,
    286       &reply);
    287     if (GNUNET_OK != ret)
    288     {
    289       GNUNET_break (0);
    290       json_decref (data);
    291       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    292     }
    293     json_decref (data);
    294   }
    295 
    296 
    297   GNUNET_break (MHD_YES ==
    298                 MHD_add_response_header (reply,
    299                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    300                                          "text/html"));
    301   /* The paywall body depends on the negotiated language and on
    302      whether we deflated it for the client; tell intermediaries to
    303      key their cache entries on both. */
    304   GNUNET_break (MHD_YES ==
    305                 MHD_add_response_header (reply,
    306                                          MHD_HTTP_HEADER_VARY,
    307                                          MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", "
    308                                          MHD_HTTP_HEADER_ACCEPT_ENCODING ", "
    309                                          "Cookie"));
    310   GNUNET_break (MHD_YES ==
    311                 MHD_add_response_header (reply,
    312                                          MHD_HTTP_HEADER_CACHE_CONTROL,
    313                                          "public, max-age=300"));
    314   {
    315     char *uri;
    316 
    317     uri = make_taler_pay_template_uri (PH_merchant_base_url,
    318                                        t->template_id);
    319     GNUNET_assert (MHD_YES ==
    320                    MHD_add_response_header (reply,
    321                                             "Paivana",
    322                                             uri));
    323     GNUNET_free (uri);
    324   }
    325 
    326   {
    327     struct ResponseCacheEntry *rce;
    328 
    329     rce = GNUNET_new (struct ResponseCacheEntry);
    330     if (NULL != lang)
    331       rce->lang = GNUNET_strdup (lang);
    332     if (NULL != ae)
    333       rce->ae = GNUNET_strdup (ae);
    334     rce->paywall = reply;
    335     rce->http_status = http_status;
    336     GNUNET_CONTAINER_DLL_insert (t->rce_head,
    337                                  t->rce_tail,
    338                                  rce);
    339     return MHD_queue_response (conn,
    340                                rce->http_status,
    341                                reply);
    342   }
    343 }
    344 
    345 
    346 /**
    347  * Parse template contract to (mostly) determine the
    348  * regex specifying which websites the template applies to.
    349  *
    350  * @param[in,out] t template to update
    351  * @param contract contract to parse
    352  */
    353 static void
    354 parse_template (struct Template *t,
    355                 const json_t *contract)
    356 {
    357   const char *regex = NULL;
    358   struct GNUNET_JSON_Specification spec[] = {
    359     GNUNET_JSON_spec_mark_optional (
    360       GNUNET_JSON_spec_string ("website_regex",
    361                                &regex),
    362       NULL),
    363     GNUNET_JSON_spec_mark_optional (
    364       GNUNET_JSON_spec_relative_time ("max_pickup_duration",
    365                                       &t->max_pickup_delay),
    366       NULL),
    367     GNUNET_JSON_spec_end ()
    368   };
    369   const char *en;
    370 
    371   if (GNUNET_OK !=
    372       GNUNET_JSON_parse ((json_t *) contract,
    373                          spec,
    374                          &en,
    375                          NULL))
    376   {
    377     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    378                 "Invalid template %s at field %s\n",
    379                 t->template_id,
    380                 en);
    381     return;
    382   }
    383   if (0 != regcomp (&t->ex,
    384                     regex,
    385                     REG_NOSUB | REG_EXTENDED))
    386   {
    387     GNUNET_break_op (0);
    388     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    389                 "Invalid regex in template %s: %s\n",
    390                 t->template_id,
    391                 regex);
    392     return;
    393   }
    394   t->regex = GNUNET_strdup (regex);
    395   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    396               "Using payment template %s for `%s'\n",
    397               t->template_id,
    398               regex);
    399 }
    400 
    401 
    402 /**
    403  * Callback for a GET /private/templates/$TEMPLATE_ID request.
    404  *
    405  * @param cls closure
    406  * @param tgr response details
    407  */
    408 static void
    409 setup_template (
    410   struct Template *t,
    411   const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr)
    412 {
    413   t->gt = NULL;
    414   switch (tgr->hr.http_status)
    415   {
    416   case MHD_HTTP_OK:
    417     parse_template (t,
    418                     tgr->details.ok.template_contract);
    419     break;
    420   default:
    421     GNUNET_break (0);
    422     break;
    423   }
    424   for (struct Template *p = t_head; NULL != p; p = p->next)
    425     if (NULL != p->gt)
    426       return;
    427   /* all templates done, continue with main logic */
    428   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    429               "Templates loaded, starting to serve requests\n");
    430   PAIVANA_HTTPD_serve_requests ();
    431 }
    432 
    433 
    434 /**
    435  * Callback for a GET /private/templates request.
    436  *
    437  * @param cls closure
    438  * @param tgr response details
    439  */
    440 static void
    441 check_templates (
    442   void *cls,
    443   const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr)
    444 {
    445   gpt = NULL;
    446   switch (tgr->hr.http_status)
    447   {
    448   case MHD_HTTP_OK:
    449     break;
    450   case MHD_HTTP_NO_CONTENT:
    451     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    452                 "No templates found, starting to serve requests\n");
    453     PAIVANA_HTTPD_serve_requests ();
    454     return;
    455   case MHD_HTTP_UNAUTHORIZED:
    456     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    457                 "Access to templates unauthorized: %s\n",
    458                 TALER_ErrorCode_get_hint (tgr->hr.ec));
    459     PH_global_ret = EXIT_FAILURE;
    460     GNUNET_SCHEDULER_shutdown ();
    461     return;
    462   default:
    463     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    464                 "Unexpected HTTP status code %u on GET /private/templates (%d)\n",
    465                 tgr->hr.http_status,
    466                 (int) tgr->hr.ec);
    467     PH_global_ret = EXIT_FAILURE;
    468     GNUNET_SCHEDULER_shutdown ();
    469     return;
    470   }
    471   if (0 == tgr->details.ok.templates_length)
    472   {
    473     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    474                 "No templates found, starting to serve requests\n");
    475     PAIVANA_HTTPD_serve_requests ();
    476     return;
    477   }
    478 
    479   for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++)
    480   {
    481     const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te
    482       = &tgr->details.ok.templates[i];
    483     struct Template *t;
    484 
    485     t = GNUNET_new (struct Template);
    486     t->template_id = GNUNET_strdup (te->template_id);
    487     t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL;
    488     t->gt = TALER_MERCHANT_get_private_template_create (PH_ctx,
    489                                                         PH_merchant_base_url,
    490                                                         t->template_id);
    491     GNUNET_CONTAINER_DLL_insert (t_head,
    492                                  t_tail,
    493                                  t);
    494     GNUNET_assert (
    495       TALER_EC_NONE ==
    496       TALER_MERCHANT_get_private_template_start (t->gt,
    497                                                  &setup_template,
    498                                                  t));
    499   }
    500 }
    501 
    502 
    503 void
    504 PAIVANA_HTTPD_load_templates ()
    505 {
    506   gpt = TALER_MERCHANT_get_private_templates_create (PH_ctx,
    507                                                      PH_merchant_base_url);
    508   GNUNET_assert (NULL != gpt);
    509   GNUNET_assert (
    510     TALER_EC_NONE ==
    511     TALER_MERCHANT_get_private_templates_start (gpt,
    512                                                 &check_templates,
    513                                                 NULL));
    514 }
    515 
    516 
    517 enum GNUNET_GenericReturnValue
    518 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
    519                                 const char *website)
    520 {
    521   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    522               "Searching templates for `%s'\n",
    523               website);
    524   for (struct Template *t = t_head; NULL != t; t = t->next)
    525   {
    526     if (NULL == t->regex)
    527       continue;
    528     if (0 == regexec (&t->ex,
    529                       website,
    530                       0, NULL,
    531                       0))
    532     {
    533       struct MHD_Response *redirect;
    534       enum MHD_Result ret;
    535       struct GNUNET_Buffer buf = { 0 };
    536       char *enc = NULL;
    537       char *url;
    538 
    539       redirect = MHD_create_response_from_buffer_static (0,
    540                                                          NULL);
    541       if (! PAIVANA_HTTPD_get_base_url (connection,
    542                                         &buf))
    543       {
    544         GNUNET_break (0);
    545         return TALER_MHD_reply_with_error (
    546           connection,
    547           MHD_HTTP_BAD_REQUEST,
    548           TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
    549           "Host or X-Forwarded-Host required");
    550       }
    551       GNUNET_STRINGS_base64url_encode (website,
    552                                        strlen (website),
    553                                        &enc);
    554       GNUNET_buffer_write_str (&buf,
    555                                "/.well-known/paivana/templates/");
    556       GNUNET_buffer_write_str (&buf,
    557                                t->template_id);
    558       GNUNET_buffer_write_str (&buf,
    559                                "#");
    560       GNUNET_buffer_write_str (&buf,
    561                                enc);
    562       GNUNET_free (enc);
    563       url = GNUNET_buffer_reap_str (&buf);
    564       GNUNET_break (MHD_YES ==
    565                     MHD_add_response_header (redirect,
    566                                              MHD_HTTP_HEADER_LOCATION,
    567                                              url));
    568       GNUNET_break (MHD_YES ==
    569                     MHD_add_response_header (redirect,
    570                                              MHD_HTTP_HEADER_VARY,
    571                                              "Cookie"));
    572       GNUNET_break (MHD_YES ==
    573                     MHD_add_response_header (redirect,
    574                                              MHD_HTTP_HEADER_CACHE_CONTROL,
    575                                              "public, max-age=60"));
    576       GNUNET_free (url);
    577       ret = MHD_queue_response (connection,
    578                                 MHD_HTTP_FOUND,
    579                                 redirect);
    580       MHD_destroy_response (redirect);
    581       return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
    582 
    583     }
    584     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    585                 "Request for %s did not match template %s\n",
    586                 website,
    587                 t->template_id);
    588   }
    589   return GNUNET_SYSERR;
    590 }
    591 
    592 
    593 /**
    594  * Return the paywall page for the given @a template.
    595  *
    596  * @param connection request to search paywall response for
    597  * @param id template to return paywall template for
    598  * @return MHD status code
    599  */
    600 enum MHD_Result
    601 PAIVANA_HTTPD_return_template (struct MHD_Connection *connection,
    602                                const char *template)
    603 {
    604   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    605               "Searching template `%s'\n",
    606               template);
    607   for (struct Template *t = t_head; NULL != t; t = t->next)
    608   {
    609     if (0 == strcmp (template,
    610                      t->template_id))
    611       return load_paywall (connection,
    612                            t);
    613   }
    614   GNUNET_break_op (0);
    615   return TALER_MHD_reply_with_error (connection,
    616                                      MHD_HTTP_NOT_FOUND,
    617                                      TALER_EC_PAIVANA_TEMPLATE_UNKNOWN,
    618                                      template);
    619 }
    620 
    621 
    622 /**
    623  * Unload all of the template state.
    624  */
    625 void
    626 PAIVANA_HTTPD_unload_templates ()
    627 {
    628   while (NULL != t_head)
    629   {
    630     struct Template *t = t_head;
    631 
    632     while (NULL != t->rce_head)
    633     {
    634       struct ResponseCacheEntry *rce = t->rce_head;
    635 
    636       GNUNET_CONTAINER_DLL_remove (t->rce_head,
    637                                    t->rce_tail,
    638                                    rce);
    639       MHD_destroy_response (rce->paywall);
    640       GNUNET_free (rce->ae);
    641       GNUNET_free (rce->lang);
    642       GNUNET_free (rce);
    643     }
    644     GNUNET_CONTAINER_DLL_remove (t_head,
    645                                  t_tail,
    646                                  t);
    647     if (NULL != t->gt)
    648       TALER_MERCHANT_get_private_template_cancel (t->gt);
    649     if (NULL != t->regex)
    650     {
    651       regfree (&t->ex);
    652       GNUNET_free (t->regex);
    653     }
    654     GNUNET_free (t->template_id);
    655     GNUNET_free (t);
    656   }
    657   if (NULL != gpt)
    658   {
    659     TALER_MERCHANT_get_private_templates_cancel (gpt);
    660     gpt = NULL;
    661   }
    662 }