paivana

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

paivana-httpd_templates.c (20145B)


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