paivana

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

paivana-httpd_templates.c (12670B)


      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_curl_lib.h>
     30 #include "paivana-httpd.h"
     31 #include "paivana-httpd_daemon.h"
     32 #include "paivana-httpd_templates.h"
     33 #include <taler/taler_mhd_lib.h>
     34 #include <taler/taler_templating_lib.h>
     35 #include "paivana_pd.h"
     36 #include <regex.h>
     37 
     38 
     39 struct Template;
     40 #define TALER_MERCHANT_GET_PRIVATE_TEMPLATE_RESULT_CLOSURE struct Template
     41 #include <taler/merchant/get-private-templates-TEMPLATE_ID.h>
     42 #include <taler/merchant/get-private-templates.h>
     43 
     44 
     45 /**
     46  * Entry in the cache of responses for a given template.
     47  */
     48 struct ResponseCacheEntry
     49 {
     50 
     51   /**
     52    * Kept in a DLL.
     53    */
     54   struct ResponseCacheEntry *next;
     55 
     56   /**
     57    * Kept in a DLL.
     58    */
     59   struct ResponseCacheEntry *prev;
     60 
     61   /**
     62    * Language of the response.
     63    */
     64   char *lang;
     65 
     66   /**
     67    * Accept-Encoding of the response.
     68    */
     69   char *ae;
     70 
     71   /**
     72    * Paywall response for these request parameters.
     73    */
     74   struct MHD_Response *paywall;
     75 
     76   /**
     77    * HTTP status to return with @e paywall.
     78    */
     79   unsigned int http_status;
     80 
     81 };
     82 
     83 
     84 /**
     85  * Information about a template in the merchant backend.
     86  */
     87 struct Template
     88 {
     89 
     90   /**
     91    * Kept in a DLL.
     92    */
     93   struct Template *next;
     94 
     95   /**
     96    * Kept in a DLL.
     97    */
     98   struct Template *prev;
     99 
    100   /**
    101    * ID of the template.
    102    */
    103   char *template_id;
    104 
    105   /**
    106    * Maximum pickup delay for the pages.
    107    */
    108   struct GNUNET_TIME_Relative max_pickup_delay;
    109 
    110   /**
    111    * Regular expression of websites the template is for.
    112    */
    113   char *regex;
    114 
    115   /**
    116    * Pre-compiled regular expression @e regex.
    117    */
    118   regex_t ex;
    119 
    120   /**
    121    * Handle used to request more information about the template.
    122    */
    123   struct TALER_MERCHANT_GetPrivateTemplateHandle *gt;
    124 
    125   /**
    126    * Kept in a DLL.
    127    */
    128   struct ResponseCacheEntry *rce_head;
    129 
    130   /**
    131    * Kept in a DLL.
    132    */
    133   struct ResponseCacheEntry *rce_tail;
    134 
    135 };
    136 
    137 
    138 /**
    139  * Kept in a DLL.
    140  */
    141 static struct Template *t_head;
    142 
    143 /**
    144  * Kept in a DLL.
    145  */
    146 static struct Template *t_tail;
    147 
    148 /**
    149  * Handle to get all the templates.
    150  */
    151 static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt;
    152 
    153 
    154 /**
    155  * Check if two strings are equal, including both being NULL
    156  *
    157  * @param s1 a string, possibly NULL
    158  * @param s2 a string. possibly NULL
    159  * @return true if both are equal
    160  */
    161 static bool
    162 eq (const char *s1,
    163     const char *s2)
    164 {
    165   if (s1 == s2)
    166     return true;
    167   if (NULL == s1)
    168     return false;
    169   if (NULL == s2)
    170     return false;
    171   return (0 == strcmp (s1,
    172                        s2));
    173 }
    174 
    175 
    176 /**
    177  * Try to initialize the paywall response.
    178  *
    179  * @param conn connection to create the response for
    180  * @param t template template to create the response for
    181  * @return MHD status code to return
    182  */
    183 static enum MHD_Result
    184 load_paywall (struct MHD_Connection *conn,
    185               struct Template *t)
    186 {
    187   struct MHD_Response *reply;
    188   const char *lang;
    189   const char *ae;
    190   unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED;
    191 
    192   lang = MHD_lookup_connection_value (conn,
    193                                       MHD_HEADER_KIND,
    194                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    195   ae = MHD_lookup_connection_value (conn,
    196                                     MHD_HEADER_KIND,
    197                                     MHD_HTTP_HEADER_ACCEPT_ENCODING);
    198   for (struct ResponseCacheEntry *pos = t->rce_head;
    199        NULL != pos;
    200        pos = pos->next)
    201   {
    202     if ( (eq (lang,
    203               pos->lang)) &&
    204          (eq (ae,
    205               pos->ae) ) )
    206       return MHD_queue_response (conn,
    207                                  pos->http_status,
    208                                  pos->paywall);
    209   }
    210 
    211   {
    212     enum GNUNET_GenericReturnValue ret;
    213     json_t *data;
    214 
    215     data = GNUNET_JSON_PACK (
    216       GNUNET_JSON_pack_string (
    217         "template_id",
    218         t->template_id),
    219       GNUNET_JSON_pack_uint64 (
    220         "max_pickup_delay",
    221         t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
    222       GNUNET_JSON_pack_string (
    223         "merchant_backend",
    224         PH_merchant_base_url));
    225     ret = TALER_TEMPLATING_build (conn,
    226                                   &http_status,
    227                                   "paywall",
    228                                   NULL /* no instance */,
    229                                   NULL, /* FIXME: no Taler URI!? */
    230                                   data,
    231                                   &reply);
    232     if (GNUNET_OK != ret)
    233     {
    234       GNUNET_break (0);
    235       json_decref (data);
    236       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    237     }
    238     json_decref (data);
    239   }
    240 
    241   GNUNET_break (MHD_YES ==
    242                 MHD_add_response_header (reply,
    243                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    244                                          "text/html"));
    245   // FIXME: set Vary and other cache control headers!
    246   {
    247     struct ResponseCacheEntry *rce;
    248 
    249     rce = GNUNET_new (struct ResponseCacheEntry);
    250     if (NULL != lang)
    251       rce->lang = GNUNET_strdup (lang);
    252     if (NULL != ae)
    253       rce->ae = GNUNET_strdup (ae);
    254     rce->paywall = reply;
    255     rce->http_status = http_status;
    256     GNUNET_CONTAINER_DLL_insert (t->rce_head,
    257                                  t->rce_tail,
    258                                  rce);
    259     return MHD_queue_response (conn,
    260                                rce->http_status,
    261                                reply);
    262   }
    263 }
    264 
    265 
    266 /**
    267  * Parse template contract to (mostly) determine the
    268  * regex specifying which websites the template applies to.
    269  *
    270  * @param[in,out] t template to update
    271  * @param contract contract to parse
    272  */
    273 static void
    274 parse_template (struct Template *t,
    275                 const json_t *contract)
    276 {
    277   const char *regex = NULL;
    278   struct GNUNET_JSON_Specification spec[] = {
    279     GNUNET_JSON_spec_mark_optional (
    280       GNUNET_JSON_spec_string ("website_regex",
    281                                &regex),
    282       NULL),
    283     GNUNET_JSON_spec_end ()
    284   };
    285   const char *en;
    286 
    287   if (GNUNET_OK !=
    288       GNUNET_JSON_parse ((json_t *) contract,
    289                          spec,
    290                          &en,
    291                          NULL))
    292   {
    293     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    294                 "Invalid template %s at field %s\n",
    295                 t->template_id,
    296                 en);
    297     return;
    298   }
    299   if (0 != regcomp (&t->ex,
    300                     regex,
    301                     REG_NOSUB | REG_EXTENDED))
    302   {
    303     GNUNET_break_op (0);
    304     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    305                 "Invalid regex in template %s: %s\n",
    306                 t->template_id,
    307                 regex);
    308     return;
    309   }
    310   t->regex = GNUNET_strdup (regex);
    311   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    312               "Using payment template %s for `%s'\n",
    313               t->template_id,
    314               regex);
    315 }
    316 
    317 
    318 /**
    319  * Callback for a GET /private/templates/$TEMPLATE_ID request.
    320  *
    321  * @param cls closure
    322  * @param tgr response details
    323  */
    324 static void
    325 setup_template (
    326   struct Template *t,
    327   const struct TALER_MERCHANT_GetPrivateTemplateResponse *tgr)
    328 {
    329   t->gt = NULL;
    330   switch (tgr->hr.http_status)
    331   {
    332   case MHD_HTTP_OK:
    333     parse_template (t,
    334                     tgr->details.ok.template_contract);
    335     break;
    336   default:
    337     GNUNET_break (0);
    338     break;
    339   }
    340   for (struct Template *p = t_head; NULL != p; p = p->next)
    341     if (NULL != p->gt)
    342       return;
    343   /* all templates done, continue with main logic */
    344   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    345               "Templates loaded, starting to serve requests\n");
    346   PAIVANA_HTTPD_serve_requests ();
    347 }
    348 
    349 
    350 /**
    351  * Callback for a GET /private/templates request.
    352  *
    353  * @param cls closure
    354  * @param tgr response details
    355  */
    356 static void
    357 check_templates (
    358   void *cls,
    359   const struct TALER_MERCHANT_GetPrivateTemplatesResponse *tgr)
    360 {
    361   gpt = NULL;
    362   switch (tgr->hr.http_status)
    363   {
    364   case MHD_HTTP_OK:
    365     break;
    366   case MHD_HTTP_NO_CONTENT:
    367     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    368                 "No templates found, starting to serve requests\n");
    369     PAIVANA_HTTPD_serve_requests ();
    370     return;
    371   case MHD_HTTP_UNAUTHORIZED:
    372     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    373                 "Access to templates unauthorized: %s\n",
    374                 TALER_ErrorCode_get_hint (tgr->hr.ec));
    375     PH_global_ret = EXIT_FAILURE;
    376     GNUNET_SCHEDULER_shutdown ();
    377     return;
    378   default:
    379     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    380                 "Unexpected HTTP status code %u on GET /private/templates (%d)\n",
    381                 tgr->hr.http_status,
    382                 (int) tgr->hr.ec);
    383     PH_global_ret = EXIT_FAILURE;
    384     GNUNET_SCHEDULER_shutdown ();
    385     return;
    386   }
    387   if (0 == tgr->details.ok.templates_length)
    388   {
    389     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    390                 "No templates found, starting to serve requests\n");
    391     PAIVANA_HTTPD_serve_requests ();
    392     return;
    393   }
    394 
    395   for (unsigned int i = 0; i<tgr->details.ok.templates_length; i++)
    396   {
    397     const struct TALER_MERCHANT_GetPrivateTemplatesTemplateEntry *te
    398       = &tgr->details.ok.templates[i];
    399     struct Template *t;
    400 
    401     t = GNUNET_new (struct Template);
    402     t->template_id = GNUNET_strdup (te->template_id);
    403     t->gt = TALER_MERCHANT_get_private_template_create (PH_ctx,
    404                                                         PH_merchant_base_url,
    405                                                         t->template_id);
    406     GNUNET_CONTAINER_DLL_insert (t_head,
    407                                  t_tail,
    408                                  t);
    409     GNUNET_assert (
    410       TALER_EC_NONE ==
    411       TALER_MERCHANT_get_private_template_start (t->gt,
    412                                                  &setup_template,
    413                                                  t));
    414   }
    415 }
    416 
    417 
    418 void
    419 PAIVANA_HTTPD_load_templates ()
    420 {
    421   gpt = TALER_MERCHANT_get_private_templates_create (PH_ctx,
    422                                                      PH_merchant_base_url);
    423   GNUNET_assert (NULL != gpt);
    424   GNUNET_assert (
    425     TALER_EC_NONE ==
    426     TALER_MERCHANT_get_private_templates_start (gpt,
    427                                                 &check_templates,
    428                                                 NULL));
    429 }
    430 
    431 
    432 enum GNUNET_GenericReturnValue
    433 PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
    434                                 const char *website)
    435 {
    436   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    437               "Searching templates for `%s'\n",
    438               website);
    439   for (struct Template *t = t_head; NULL != t; t = t->next)
    440   {
    441     if (NULL == t->regex)
    442       continue;
    443     if (0 == regexec (&t->ex,
    444                       website,
    445                       0, NULL,
    446                       0))
    447     {
    448       enum MHD_Result ret;
    449 
    450       ret = load_paywall (connection,
    451                           t);
    452       return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
    453     }
    454     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    455                 "Request for %s did not match template %s\n",
    456                 website,
    457                 t->template_id);
    458   }
    459   return GNUNET_SYSERR;
    460 }
    461 
    462 
    463 /**
    464  * Unload all of the template state.
    465  */
    466 void
    467 PAIVANA_HTTPD_unload_templates ()
    468 {
    469   while (NULL != t_head)
    470   {
    471     struct Template *t = t_head;
    472 
    473     while (NULL != t->rce_head)
    474     {
    475       struct ResponseCacheEntry *rce = t->rce_head;
    476 
    477       GNUNET_CONTAINER_DLL_remove (t->rce_head,
    478                                    t->rce_tail,
    479                                    rce);
    480       MHD_destroy_response (rce->paywall);
    481       GNUNET_free (rce->ae);
    482       GNUNET_free (rce->lang);
    483       GNUNET_free (rce);
    484     }
    485     GNUNET_CONTAINER_DLL_remove (t_head,
    486                                  t_tail,
    487                                  t);
    488     if (NULL != t->gt)
    489       TALER_MERCHANT_get_private_template_cancel (t->gt);
    490     if (NULL != t->regex)
    491     {
    492       regfree (&t->ex);
    493       GNUNET_free (t->regex);
    494     }
    495     GNUNET_free (t->template_id);
    496     GNUNET_free (t);
    497   }
    498   if (NULL != gpt)
    499   {
    500     TALER_MERCHANT_get_private_templates_cancel (gpt);
    501     gpt = NULL;
    502   }
    503 }