paivana

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

paivana-httpd_templates.c (19333B)


      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_array_incref (
    288         "choices",
    289         t->choices),
    290       GNUNET_JSON_pack_bool (
    291         "has_choices",
    292         1 < json_array_size(t->choices)),
    293       GNUNET_JSON_pack_object_steal (
    294         "default_choice",
    295         json_array_get(t->choices, 0)),
    296       GNUNET_JSON_pack_uint64 (
    297         "max_pickup_delay",
    298         t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
    299       GNUNET_JSON_pack_string (
    300         "merchant_backend",
    301         PH_merchant_base_url));
    302     ret = TALER_TEMPLATING_build (
    303       conn,
    304       &http_status,
    305       "paywall",
    306       NULL /* no instance */,
    307       NULL /* no Taler URI (needs dynamic paivana_id!) */,
    308       data,
    309       &reply);
    310     if (GNUNET_OK != ret)
    311     {
    312       GNUNET_break (0);
    313       json_decref (data);
    314       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    315     }
    316     json_decref (data);
    317   }
    318 
    319 
    320   GNUNET_break (MHD_YES ==
    321                 MHD_add_response_header (reply,
    322                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    323                                          "text/html"));
    324   /* The paywall body depends on the negotiated language and on
    325      whether we deflated it for the client; tell intermediaries to
    326      key their cache entries on both. */
    327   GNUNET_break (MHD_YES ==
    328                 MHD_add_response_header (reply,
    329                                          MHD_HTTP_HEADER_VARY,
    330                                          MHD_HTTP_HEADER_ACCEPT_LANGUAGE ", "
    331                                          MHD_HTTP_HEADER_ACCEPT_ENCODING ", "
    332                                          "Cookie"));
    333   GNUNET_break (MHD_YES ==
    334                 MHD_add_response_header (reply,
    335                                          MHD_HTTP_HEADER_CACHE_CONTROL,
    336                                          "public, max-age=300"));
    337   {
    338     char *uri;
    339 
    340     uri = make_taler_pay_template_uri (PH_merchant_base_url,
    341                                        t->template_id);
    342     GNUNET_assert (MHD_YES ==
    343                    MHD_add_response_header (reply,
    344                                             "Paivana",
    345                                             uri));
    346     GNUNET_free (uri);
    347   }
    348 
    349   {
    350     struct ResponseCacheEntry *rce;
    351 
    352     rce = GNUNET_new (struct ResponseCacheEntry);
    353     if (NULL != lang)
    354       rce->lang = GNUNET_strdup (lang);
    355     if (NULL != ae)
    356       rce->ae = GNUNET_strdup (ae);
    357     rce->paywall = reply;
    358     rce->http_status = http_status;
    359     GNUNET_CONTAINER_DLL_insert (t->rce_head,
    360                                  t->rce_tail,
    361                                  rce);
    362     return MHD_queue_response (conn,
    363                                rce->http_status,
    364                                reply);
    365   }
    366 }
    367 
    368 
    369 /**
    370  * Parse template contract to (mostly) determine the
    371  * regex specifying which websites the template applies to.
    372  *
    373  * @param[in,out] t template to update
    374  * @param contract contract to parse
    375  */
    376 static void
    377 parse_template (struct Template *t,
    378                 const json_t *contract)
    379 {
    380   const char *regex = NULL;
    381   const char *summary = NULL;
    382   const json_t *choices = NULL;
    383   struct GNUNET_JSON_Specification spec[] = {
    384     GNUNET_JSON_spec_mark_optional (
    385       GNUNET_JSON_spec_string ("website_regex",
    386                                &regex),
    387       NULL),
    388     GNUNET_JSON_spec_mark_optional (
    389       GNUNET_JSON_spec_string ("summary",
    390                                &summary),
    391       NULL),
    392     GNUNET_JSON_spec_array_const ("choices",
    393                                   &choices),
    394     GNUNET_JSON_spec_mark_optional (
    395       GNUNET_JSON_spec_relative_time ("max_pickup_duration",
    396                                       &t->max_pickup_delay),
    397       NULL),
    398     GNUNET_JSON_spec_end ()
    399   };
    400   const char *en;
    401 
    402   if (GNUNET_OK !=
    403       GNUNET_JSON_parse ((json_t *) contract,
    404                          spec,
    405                          &en,
    406                          NULL))
    407   {
    408     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    409                 "Invalid template %s at field %s\n",
    410                 t->template_id,
    411                 en);
    412     return;
    413   }
    414   if (NULL != regex)
    415   {
    416     if (0 != regcomp (&t->ex,
    417                       regex,
    418                       REG_NOSUB | REG_EXTENDED))
    419     {
    420       GNUNET_break_op (0);
    421       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    422                   "Invalid regex in template %s: %s\n",
    423                   t->template_id,
    424                   regex);
    425       return;
    426     }
    427     t->regex = GNUNET_strdup (regex);
    428   }
    429   if (NULL != summary)
    430     t->summary = GNUNET_strdup (summary);
    431   t->choices = json_incref ((json_t *) choices);
    432   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    433               "Using payment template %s for `%s'\n",
    434               t->template_id,
    435               regex);
    436 }
    437 
    438 
    439 /**
    440  * Callback for a GET /private/templates/$TEMPLATE_ID request.
    441  *
    442  * @param cls closure
    443  * @param tgr response details
    444  */
    445 static void
    446 setup_template (
    447   struct Template *t,
    448   const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr)
    449 {
    450   t->gt = NULL;
    451   switch (tgr->hr.http_status)
    452   {
    453   case MHD_HTTP_OK:
    454     parse_template (t,
    455                     tgr->details.ok.template_contract);
    456     break;
    457   default:
    458     GNUNET_break (0);
    459     break;
    460   }
    461   for (struct Template *p = t_head; NULL != p; p = p->next)
    462     if (NULL != p->gt)
    463       return;
    464   /* all templates done, continue with main logic */
    465   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    466               "Templates loaded, starting to serve requests\n");
    467   PAIVANA_HTTPD_serve_requests ();
    468 }
    469 
    470 
    471 /**
    472  * Callback for a GET /private/templates request.
    473  *
    474  * @param cls closure
    475  * @param tgr response details
    476  */
    477 static void
    478 check_templates (
    479   void *cls,
    480   const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr)
    481 {
    482   gpt = NULL;
    483   switch (tgr->hr.http_status)
    484   {
    485   case MHD_HTTP_OK:
    486     break;
    487   case MHD_HTTP_NO_CONTENT:
    488     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    489                 "No templates found, starting to serve requests\n");
    490     PAIVANA_HTTPD_serve_requests ();
    491     return;
    492   case MHD_HTTP_UNAUTHORIZED:
    493     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    494                 "Access to templates unauthorized: %s\n",
    495                 TALER_ErrorCode_get_hint (tgr->hr.ec));
    496     PH_global_ret = EXIT_FAILURE;
    497     GNUNET_SCHEDULER_shutdown ();
    498     return;
    499   default:
    500     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    501                 "Unexpected HTTP status code %u on GET /private/templates (%d)\n",
    502                 tgr->hr.http_status,
    503                 (int) tgr->hr.ec);
    504     PH_global_ret = EXIT_FAILURE;
    505     GNUNET_SCHEDULER_shutdown ();
    506     return;
    507   }
    508   if (0 == tgr->details.ok.templates_length)
    509   {
    510     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    511                 "No templates found, starting to serve requests\n");
    512     PAIVANA_HTTPD_serve_requests ();
    513     return;
    514   }
    515 
    516   for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++)
    517   {
    518     const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te
    519       = &tgr->details.ok.templates[i];
    520     struct Template *t;
    521 
    522     t = GNUNET_new (struct Template);
    523     t->template_id = GNUNET_strdup (te->template_id);
    524     t->max_pickup_delay = GNUNET_TIME_UNIT_FOREVER_REL;
    525     t->gt = TALER_MERCHANT_get_private_template_create (PH_ctx,
    526                                                         PH_merchant_base_url,
    527                                                         t->template_id);
    528     GNUNET_CONTAINER_DLL_insert (t_head,
    529                                  t_tail,
    530                                  t);
    531     GNUNET_assert (
    532       TALER_EC_NONE ==
    533       TALER_MERCHANT_get_private_template_start (t->gt,
    534                                                  &setup_template,
    535                                                  t));
    536   }
    537 }
    538 
    539 
    540 void
    541 PAIVANA_HTTPD_load_templates ()
    542 {
    543   gpt = TALER_MERCHANT_get_private_templates_create (PH_ctx,
    544                                                      PH_merchant_base_url);
    545   GNUNET_assert (NULL != gpt);
    546   GNUNET_assert (
    547     TALER_EC_NONE ==
    548     TALER_MERCHANT_get_private_templates_start (gpt,
    549                                                 &check_templates,
    550                                                 NULL));
    551 }
    552 
    553 
    554 enum GNUNET_GenericReturnValue
    555 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
    556                                 const char *website)
    557 {
    558   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    559               "Searching templates for `%s'\n",
    560               website);
    561   for (struct Template *t = t_head; NULL != t; t = t->next)
    562   {
    563     struct MHD_Response *redirect;
    564     enum MHD_Result ret;
    565     struct GNUNET_Buffer buf = { 0 };
    566     char *enc = NULL;
    567     char *url;
    568 
    569     if ( (NULL != t->regex) &&
    570          (0 != regexec (&t->ex,
    571                         website,
    572                         0, NULL,
    573                         0)) )
    574     {
    575       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    576                   "Request for %s did not match template %s\n",
    577                   website,
    578                   t->template_id);
    579       continue;
    580     }
    581 
    582     redirect = MHD_create_response_from_buffer_static (0,
    583                                                        NULL);
    584     if (! PAIVANA_HTTPD_get_base_url (connection,
    585                                       &buf))
    586     {
    587       GNUNET_break (0);
    588       return TALER_MHD_reply_with_error (
    589         connection,
    590         MHD_HTTP_BAD_REQUEST,
    591         TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
    592         "Host or X-Forwarded-Host required");
    593     }
    594     GNUNET_STRINGS_base64url_encode (website,
    595                                      strlen (website),
    596                                      &enc);
    597     GNUNET_buffer_write_str (&buf,
    598                              "/.well-known/paivana/templates/");
    599     GNUNET_buffer_write_str (&buf,
    600                              t->template_id);
    601     GNUNET_buffer_write_str (&buf,
    602                              "#");
    603     GNUNET_buffer_write_str (&buf,
    604                              enc);
    605     GNUNET_free (enc);
    606     url = GNUNET_buffer_reap_str (&buf);
    607     GNUNET_break (MHD_YES ==
    608                   MHD_add_response_header (redirect,
    609                                            MHD_HTTP_HEADER_LOCATION,
    610                                            url));
    611     GNUNET_break (MHD_YES ==
    612                   MHD_add_response_header (redirect,
    613                                            MHD_HTTP_HEADER_VARY,
    614                                            "Cookie"));
    615     GNUNET_break (MHD_YES ==
    616                   MHD_add_response_header (redirect,
    617                                            MHD_HTTP_HEADER_CACHE_CONTROL,
    618                                            "public, max-age=60"));
    619     GNUNET_free (url);
    620     ret = MHD_queue_response (connection,
    621                               MHD_HTTP_FOUND,
    622                               redirect);
    623     MHD_destroy_response (redirect);
    624     return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
    625   }
    626   return GNUNET_SYSERR;
    627 }
    628 
    629 
    630 /**
    631  * Return the paywall page for the given @a template.
    632  *
    633  * @param connection request to search paywall response for
    634  * @param id template to return paywall template for
    635  * @return MHD status code
    636  */
    637 enum MHD_Result
    638 PAIVANA_HTTPD_return_template (struct MHD_Connection *connection,
    639                                const char *template)
    640 {
    641   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    642               "Searching template `%s'\n",
    643               template);
    644   for (struct Template *t = t_head; NULL != t; t = t->next)
    645   {
    646     if (0 == strcmp (template,
    647                      t->template_id))
    648       return load_paywall (connection,
    649                            t);
    650   }
    651   GNUNET_break_op (0);
    652   return TALER_MHD_reply_with_error (connection,
    653                                      MHD_HTTP_NOT_FOUND,
    654                                      TALER_EC_PAIVANA_TEMPLATE_UNKNOWN,
    655                                      template);
    656 }
    657 
    658 
    659 /**
    660  * Unload all of the template state.
    661  */
    662 void
    663 PAIVANA_HTTPD_unload_templates ()
    664 {
    665   while (NULL != t_head)
    666   {
    667     struct Template *t = t_head;
    668 
    669     while (NULL != t->rce_head)
    670     {
    671       struct ResponseCacheEntry *rce = t->rce_head;
    672 
    673       GNUNET_CONTAINER_DLL_remove (t->rce_head,
    674                                    t->rce_tail,
    675                                    rce);
    676       MHD_destroy_response (rce->paywall);
    677       GNUNET_free (rce->ae);
    678       GNUNET_free (rce->lang);
    679       GNUNET_free (rce);
    680     }
    681     GNUNET_CONTAINER_DLL_remove (t_head,
    682                                  t_tail,
    683                                  t);
    684     if (NULL != t->gt)
    685       TALER_MERCHANT_get_private_template_cancel (t->gt);
    686     if (NULL != t->regex)
    687     {
    688       regfree (&t->ex);
    689       GNUNET_free (t->regex);
    690     }
    691     GNUNET_free (t->template_id);
    692     GNUNET_free (t->summary);
    693     json_decref (t->choices);
    694     GNUNET_free (t);
    695   }
    696   if (NULL != gpt)
    697   {
    698     TALER_MERCHANT_get_private_templates_cancel (gpt);
    699     gpt = NULL;
    700   }
    701 }