exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

templating_api.c (16493B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2020, 2022 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file templating_api.c
     18  * @brief logic to load and complete HTML templates
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"  /* UNNECESSARY? */
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include "taler/taler_util.h"
     24 #include "taler/taler_mhd_lib.h"
     25 #include "taler/taler_templating_lib.h"
     26 #include "mustach.h"
     27 #include "mustach-jansson.h"
     28 #include <gnunet/gnunet_mhd_compat.h>
     29 
     30 
     31 /**
     32  * Entry in a key-value array we use to cache templates.
     33  */
     34 struct TVE
     35 {
     36   /**
     37    * A name, used as the key. NULL for the last entry.
     38    */
     39   char *name;
     40 
     41   /**
     42    * Language the template is in.
     43    */
     44   char *lang;
     45 
     46   /**
     47    * 0-terminated (!) file data to return for @e name and @e lang.
     48    */
     49   char *value;
     50 
     51 };
     52 
     53 
     54 /**
     55  * Array of templates loaded into RAM.
     56  */
     57 static struct TVE *loaded;
     58 
     59 /**
     60  * Length of the #loaded array.
     61  */
     62 static unsigned int loaded_length;
     63 
     64 
     65 /**
     66  * Load Mustach template into memory.  Note that we intentionally cache
     67  * failures, that is if we ever failed to load a template, we will never try
     68  * again.
     69  *
     70  * @param connection the connection we act upon
     71  * @param name name of the template file to load
     72  *        (MUST be a 'static' string in memory!)
     73  * @return NULL on error, otherwise the template
     74  */
     75 static struct TVE *
     76 lookup_template (struct MHD_Connection *connection,
     77                  const char *name)
     78 {
     79   struct TVE *best = NULL;
     80   struct TVE *fallback = NULL;
     81   double best_q = 0.0;
     82   const char *lang;
     83 
     84   lang = MHD_lookup_connection_value (connection,
     85                                       MHD_HEADER_KIND,
     86                                       MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
     87   if (NULL == lang)
     88     lang = "en";
     89   /* find best match by language */
     90   for (unsigned int i = 0; i<loaded_length; i++)
     91   {
     92     double q;
     93 
     94     if (0 != strcmp (loaded[i].name,
     95                      name))
     96       continue; /* does not match by name */
     97     if (NULL == loaded[i].lang) /* no language == always best match */
     98       return &loaded[i];
     99     if (NULL == fallback)
    100       fallback = &loaded[i]; /* in case nothing matches the language */
    101     q = TALER_pattern_matches (lang,
    102                                loaded[i].lang);
    103     if (q <= best_q)
    104       continue; /* not a strictly better match than what we have */
    105     best_q = q;
    106     best = &loaded[i];
    107   }
    108   if (NULL == best)
    109     best = fallback; /* no variant matched the language, serve any */
    110   if (NULL == best)
    111   {
    112     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    113                 "No templates found for `%s'\n",
    114                 name);
    115     return NULL;
    116   }
    117   return best;
    118 }
    119 
    120 
    121 /**
    122  * Get the base URL for static resources.
    123  *
    124  * @param con the MHD connection
    125  * @param instance_id the instance ID
    126  * @returns the static files base URL, guaranteed
    127  *          to have a trailing slash.
    128  */
    129 static char *
    130 make_static_url (struct MHD_Connection *con,
    131                  const char *instance_id)
    132 {
    133   const char *host;
    134   const char *forwarded_host;
    135   const char *uri_path;
    136   struct GNUNET_Buffer buf = { 0 };
    137 
    138   host = MHD_lookup_connection_value (con,
    139                                       MHD_HEADER_KIND,
    140                                       "Host");
    141   forwarded_host = MHD_lookup_connection_value (con,
    142                                                 MHD_HEADER_KIND,
    143                                                 "X-Forwarded-Host");
    144 
    145   uri_path = MHD_lookup_connection_value (con,
    146                                           MHD_HEADER_KIND,
    147                                           "X-Forwarded-Prefix");
    148   if (NULL != forwarded_host)
    149     host = forwarded_host;
    150 
    151   if (NULL == host)
    152   {
    153     GNUNET_break (0);
    154     return NULL;
    155   }
    156 
    157   GNUNET_assert (NULL != instance_id);
    158 
    159   if (GNUNET_NO == TALER_mhd_is_https (con))
    160     GNUNET_buffer_write_str (&buf,
    161                              "http://");
    162   else
    163     GNUNET_buffer_write_str (&buf,
    164                              "https://");
    165   GNUNET_buffer_write_str (&buf,
    166                            host);
    167   if (NULL != uri_path)
    168     GNUNET_buffer_write_path (&buf,
    169                               uri_path);
    170   if (0 != strcmp ("default",
    171                    instance_id))
    172   {
    173     GNUNET_buffer_write_path (&buf,
    174                               "instances");
    175     GNUNET_buffer_write_path (&buf,
    176                               instance_id);
    177   }
    178   GNUNET_buffer_write_path (&buf,
    179                             "static/");
    180   return GNUNET_buffer_reap_str (&buf);
    181 }
    182 
    183 
    184 int
    185 TALER_TEMPLATING_fill (const char *tmpl,
    186                        const json_t *root,
    187                        void **result,
    188                        size_t *result_size)
    189 {
    190   int eno;
    191 
    192   if (0 !=
    193       (eno = mustach_jansson_mem (tmpl,
    194                                   0, /* length of tmpl */
    195                                   (json_t *) root,
    196                                   Mustach_With_AllExtensions,
    197                                   (char **) result,
    198                                   result_size)))
    199   {
    200     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    201                 "mustach failed on template with error %d\n",
    202                 eno);
    203     *result = NULL;
    204     *result_size = 0;
    205     return eno;
    206   }
    207   return eno;
    208 }
    209 
    210 
    211 int
    212 TALER_TEMPLATING_fill2 (const void *tmpl,
    213                         size_t tmpl_len,
    214                         const json_t *root,
    215                         void **result,
    216                         size_t *result_size)
    217 {
    218   int eno;
    219 
    220   if (0 !=
    221       (eno = mustach_jansson_mem (tmpl,
    222                                   tmpl_len,
    223                                   (json_t *) root,
    224                                   Mustach_With_AllExtensions,
    225                                   (char **) result,
    226                                   result_size)))
    227   {
    228     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    229                 "mustach failed on template with error %d\n",
    230                 eno);
    231     *result = NULL;
    232     *result_size = 0;
    233     return eno;
    234   }
    235   return eno;
    236 }
    237 
    238 
    239 enum GNUNET_GenericReturnValue
    240 TALER_TEMPLATING_build (struct MHD_Connection *connection,
    241                         unsigned int *http_status,
    242                         const char *template,
    243                         const char *instance_id,
    244                         const char *taler_uri,
    245                         const json_t *root,
    246                         struct MHD_Response **reply)
    247 {
    248   char *body;
    249   size_t body_size;
    250   struct TVE *tve;
    251 
    252   {
    253     const char *tmpl;
    254     int eno;
    255 
    256     tve = lookup_template (connection,
    257                            template);
    258     if (NULL == tve)
    259     {
    260       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    261                   "Failed to load template `%s'\n",
    262                   template);
    263       *http_status = MHD_HTTP_NOT_ACCEPTABLE;
    264       *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE,
    265                                      template);
    266       return GNUNET_NO;
    267     }
    268     tmpl = tve->value;
    269     /* Add default values to the context */
    270     if (NULL != instance_id)
    271     {
    272       char *static_url = make_static_url (connection,
    273                                           instance_id);
    274 
    275       if (NULL != static_url)
    276       {
    277         GNUNET_break (0 ==
    278                       json_object_set_new ((json_t *) root,
    279                                            "static_url",
    280                                            json_string (static_url)));
    281         GNUNET_free (static_url);
    282       }
    283     }
    284     if (0 !=
    285         (eno = mustach_jansson_mem (tmpl,
    286                                     0,
    287                                     (json_t *) root,
    288                                     Mustach_With_NoExtensions,
    289                                     &body,
    290                                     &body_size)))
    291     {
    292       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    293                   "mustach failed on template `%s' with error %d\n",
    294                   template,
    295                   eno);
    296       *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    297       *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
    298                                      template);
    299       return GNUNET_NO;
    300     }
    301   }
    302 
    303   /* try to compress reply if client allows it */
    304   {
    305     bool compressed = false;
    306 
    307     if (TALER_MHD_CT_DEFLATE ==
    308         TALER_MHD_can_compress (connection,
    309                                 TALER_MHD_CT_DEFLATE))
    310     {
    311       compressed = TALER_MHD_body_compress ((void **) &body,
    312                                             &body_size);
    313     }
    314     *reply = MHD_create_response_from_buffer (body_size,
    315                                               body,
    316                                               MHD_RESPMEM_MUST_FREE);
    317     if (NULL == *reply)
    318     {
    319       GNUNET_free (body);
    320       GNUNET_break (0);
    321       return GNUNET_SYSERR;
    322     }
    323     if (compressed)
    324     {
    325       if (MHD_NO ==
    326           MHD_add_response_header (*reply,
    327                                    MHD_HTTP_HEADER_CONTENT_ENCODING,
    328                                    "deflate"))
    329       {
    330         GNUNET_break (0);
    331         MHD_destroy_response (*reply);
    332         *reply = NULL;
    333         return GNUNET_SYSERR;
    334       }
    335     }
    336   }
    337   if (NULL != tve->lang)
    338     GNUNET_break (MHD_YES ==
    339                   MHD_add_response_header (*reply,
    340                                            MHD_HTTP_HEADER_CONTENT_LANGUAGE,
    341                                            tve->lang));
    342 
    343   /* Add standard headers */
    344   if (NULL != taler_uri)
    345     GNUNET_break (MHD_NO !=
    346                   MHD_add_response_header (*reply,
    347                                            "Taler",
    348                                            taler_uri));
    349   return GNUNET_OK;
    350 }
    351 
    352 
    353 enum GNUNET_GenericReturnValue
    354 TALER_TEMPLATING_reply (struct MHD_Connection *connection,
    355                         unsigned int http_status,
    356                         const char *template,
    357                         const char *instance_id,
    358                         const char *taler_uri,
    359                         const json_t *root)
    360 {
    361   enum GNUNET_GenericReturnValue res;
    362   struct MHD_Response *reply;
    363   enum MHD_Result ret;
    364 
    365   res = TALER_TEMPLATING_build (connection,
    366                                 &http_status,
    367                                 template,
    368                                 instance_id,
    369                                 taler_uri,
    370                                 root,
    371                                 &reply);
    372   if (GNUNET_SYSERR == res)
    373     return res;
    374   if (GNUNET_OK == res)
    375     GNUNET_break (MHD_NO !=
    376                   MHD_add_response_header (reply,
    377                                            MHD_HTTP_HEADER_CONTENT_TYPE,
    378                                            "text/html"));
    379   /* Note: on failure, @a reply is a JSON error message
    380      which already comes with its own content type. */
    381   /* FIXME: also vary by compression? */
    382   GNUNET_break (MHD_NO !=
    383                 MHD_add_response_header (
    384                   reply,
    385                   MHD_HTTP_HEADER_VARY,
    386                   MHD_HTTP_HEADER_ACCEPT_LANGUAGE));
    387   ret = MHD_queue_response (connection,
    388                             http_status,
    389                             reply);
    390   MHD_destroy_response (reply);
    391   if (MHD_NO == ret)
    392     return GNUNET_SYSERR;
    393   return (res == GNUNET_OK)
    394     ? GNUNET_OK
    395     : GNUNET_NO;
    396 }
    397 
    398 
    399 /**
    400  * Function called with a template's filename.
    401  *
    402  * @param cls closure, NULL
    403  * @param filename complete filename (absolute path)
    404  * @return #GNUNET_OK to continue to iterate,
    405  *  #GNUNET_NO to stop iteration with no error,
    406  *  #GNUNET_SYSERR to abort iteration with error!
    407  */
    408 static enum GNUNET_GenericReturnValue
    409 load_template (void *cls,
    410                const char *filename)
    411 {
    412   const char *lang;
    413   const char *end;
    414   int fd;
    415   struct stat sb;
    416   char *map;
    417   const char *name;
    418 
    419   (void) cls;
    420   if ('.' == filename[0])
    421     return GNUNET_OK;
    422   name = strrchr (filename,
    423                   '/');
    424   if (NULL == name)
    425     name = filename;
    426   else
    427     name++;
    428   lang = strchr (name,
    429                  '.');
    430   if (NULL == lang)
    431     return GNUNET_OK; /* name must include .$LANG */
    432   lang++;
    433   end = strchr (lang,
    434                 '.');
    435   if ( (NULL == end) ||
    436        (0 != strcmp (end,
    437                      ".must")) )
    438     return GNUNET_OK; /* name must end with '.must' */
    439 
    440   /* finally open template */
    441   fd = open (filename,
    442              O_RDONLY);
    443   if (-1 == fd)
    444   {
    445     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    446                               "open",
    447                               filename);
    448 
    449     return GNUNET_SYSERR;
    450   }
    451   if (0 !=
    452       fstat (fd,
    453              &sb))
    454   {
    455     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    456                               "fstat",
    457                               filename);
    458     GNUNET_break (0 == close (fd));
    459     return GNUNET_OK;
    460   }
    461   map = GNUNET_malloc_large (sb.st_size + 1);
    462   if (NULL == map)
    463   {
    464     GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    465                          "malloc");
    466     GNUNET_break (0 == close (fd));
    467     return GNUNET_SYSERR;
    468   }
    469   if (sb.st_size !=
    470       read (fd,
    471             map,
    472             sb.st_size))
    473   {
    474     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    475                               "read",
    476                               filename);
    477     GNUNET_free (map);
    478     GNUNET_break (0 == close (fd));
    479     return GNUNET_OK;
    480   }
    481   GNUNET_break (0 == close (fd));
    482   GNUNET_array_grow (loaded,
    483                      loaded_length,
    484                      loaded_length + 1);
    485   loaded[loaded_length - 1].name = GNUNET_strndup (name,
    486                                                    (lang - 1) - name);
    487   loaded[loaded_length - 1].lang = GNUNET_strndup (lang,
    488                                                    end - lang);
    489   loaded[loaded_length - 1].value = map;
    490   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    491               "Loading template `%s' (%s)\n",
    492               filename,
    493               loaded[loaded_length - 1].name);
    494   return GNUNET_OK;
    495 }
    496 
    497 
    498 enum MHD_Result
    499 TALER_TEMPLATING_reply_error (
    500   struct MHD_Connection *connection,
    501   const char *template_basename,
    502   unsigned int http_status,
    503   enum TALER_ErrorCode ec,
    504   const char *detail)
    505 {
    506   json_t *data;
    507   enum GNUNET_GenericReturnValue ret;
    508 
    509   data = GNUNET_JSON_PACK (
    510     GNUNET_JSON_pack_uint64 ("ec",
    511                              ec),
    512     GNUNET_JSON_pack_string ("hint",
    513                              TALER_ErrorCode_get_hint (ec)),
    514     GNUNET_JSON_pack_allow_null (
    515       GNUNET_JSON_pack_string ("detail",
    516                                detail))
    517     );
    518   ret = TALER_TEMPLATING_reply (connection,
    519                                 http_status,
    520                                 template_basename,
    521                                 NULL,
    522                                 NULL,
    523                                 data);
    524   json_decref (data);
    525   switch (ret)
    526   {
    527   case GNUNET_OK:
    528     return MHD_YES;
    529   case GNUNET_NO:
    530     return MHD_YES;
    531   case GNUNET_SYSERR:
    532     return MHD_NO;
    533   }
    534   GNUNET_assert (0);
    535   return MHD_NO;
    536 }
    537 
    538 
    539 enum GNUNET_GenericReturnValue
    540 TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd)
    541 {
    542   char *dn;
    543   int ret;
    544 
    545   {
    546     char *path;
    547 
    548     path = GNUNET_OS_installation_get_path (pd,
    549                                             GNUNET_OS_IPK_DATADIR);
    550     if (NULL == path)
    551     {
    552       GNUNET_break (0);
    553       return GNUNET_SYSERR;
    554     }
    555     GNUNET_asprintf (&dn,
    556                      "%s/templates/",
    557                      path);
    558     GNUNET_free (path);
    559   }
    560   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    561               "Loading templates from `%s'\n",
    562               dn);
    563   ret = GNUNET_DISK_directory_scan (dn,
    564                                     &load_template,
    565                                     NULL);
    566   GNUNET_free (dn);
    567   if (-1 == ret)
    568   {
    569     GNUNET_break (0);
    570     return GNUNET_SYSERR;
    571   }
    572   return GNUNET_OK;
    573 }
    574 
    575 
    576 void
    577 TALER_TEMPLATING_done (void)
    578 {
    579   for (unsigned int i = 0; i<loaded_length; i++)
    580   {
    581     GNUNET_free (loaded[i].name);
    582     GNUNET_free (loaded[i].lang);
    583     GNUNET_free (loaded[i].value);
    584   }
    585   GNUNET_array_grow (loaded,
    586                      loaded_length,
    587                      0);
    588 }
    589 
    590 
    591 /* end of templating_api.c */