exchange

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

mhd_legal.c (19935B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2019, 2020, 2022, 2024 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero 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 Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file mhd_legal.c
     18  * @brief API for returning legal documents based on client language
     19  *        and content type preferences
     20  * @author Christian Grothoff
     21  */
     22 #include "taler/platform.h"
     23 #include <gnunet/gnunet_util_lib.h>
     24 #include <gnunet/gnunet_json_lib.h>
     25 #include <jansson.h>
     26 #include <microhttpd.h>
     27 #include "taler/taler_util.h"
     28 #include "taler/taler_mhd_lib.h"
     29 
     30 /**
     31  * How long should browsers/proxies cache the "legal" replies?
     32  */
     33 #define MAX_TERMS_CACHING GNUNET_TIME_UNIT_DAYS
     34 
     35 /**
     36  * HTTP header with the version of the terms of service.
     37  */
     38 #define TALER_TERMS_VERSION "Taler-Terms-Version"
     39 
     40 /**
     41  * Entry in the terms-of-service array.
     42  */
     43 struct Terms
     44 {
     45   /**
     46    * Kept in a DLL.
     47    */
     48   struct Terms *prev;
     49 
     50   /**
     51    * Kept in a DLL.
     52    */
     53   struct Terms *next;
     54 
     55   /**
     56    * Mime type of the terms.
     57    */
     58   const char *mime_type;
     59 
     60   /**
     61    * The terms (NOT 0-terminated!), mmap()'ed. Do not free,
     62    * use munmap() instead.
     63    */
     64   void *terms;
     65 
     66   /**
     67    * The desired language.
     68    */
     69   char *language;
     70 
     71   /**
     72    * deflated @e terms, to return if client supports deflate compression.
     73    * malloc()'ed.  NULL if @e terms does not compress.
     74    */
     75   void *compressed_terms;
     76 
     77   /**
     78    * Etag we use for this response.
     79    */
     80   char *terms_etag;
     81 
     82   /**
     83    * Number of bytes in @e terms.
     84    */
     85   size_t terms_size;
     86 
     87   /**
     88    * Number of bytes in @e compressed_terms.
     89    */
     90   size_t compressed_terms_size;
     91 
     92   /**
     93    * Sorting key by format preference in case
     94    * everything else is equal. Higher is preferred.
     95    */
     96   unsigned int priority;
     97 
     98 };
     99 
    100 
    101 /**
    102  * Prepared responses for legal documents
    103  * (terms of service, privacy policy).
    104  */
    105 struct TALER_MHD_Legal
    106 {
    107   /**
    108    * DLL of terms of service.
    109    */
    110   struct Terms *terms_head;
    111 
    112   /**
    113    * DLL of terms of service.
    114    */
    115   struct Terms *terms_tail;
    116 
    117   /**
    118    * Etag to use for the terms of service (= version).
    119    */
    120   char *terms_version;
    121 };
    122 
    123 
    124 MHD_RESULT
    125 TALER_MHD_reply_legal (struct MHD_Connection *conn,
    126                        struct TALER_MHD_Legal *legal)
    127 {
    128   /* Default terms of service if none are configured */
    129   static struct Terms none = {
    130     .mime_type = "text/plain",
    131     .terms = (void *) "not configured",
    132     .language = (void *) "en",
    133     .terms_size = strlen ("not configured")
    134   };
    135   struct MHD_Response *resp;
    136   struct Terms *t;
    137   struct GNUNET_TIME_Absolute a;
    138   struct GNUNET_TIME_Timestamp m;
    139   char dat[128];
    140   char *langs;
    141 
    142   t = NULL;
    143   langs = NULL;
    144 
    145   a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING);
    146   m = GNUNET_TIME_absolute_to_timestamp (a);
    147   /* Round up to next full day to ensure the expiration
    148      time does not become a fingerprint! */
    149   a = GNUNET_TIME_absolute_round_down (a,
    150                                        MAX_TERMS_CACHING);
    151   a = GNUNET_TIME_absolute_add (a,
    152                                 MAX_TERMS_CACHING);
    153   TALER_MHD_get_date_string (m.abs_time,
    154                              dat);
    155   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    156               "Setting '%s' header to '%s'\n",
    157               MHD_HTTP_HEADER_EXPIRES,
    158               dat);
    159   if (NULL == legal)
    160   {
    161     t = &none;
    162     goto return_t;
    163   }
    164 
    165   if (NULL != legal)
    166   {
    167     const char *mime;
    168     const char *lang;
    169     double best_mime_q = 0.0;
    170     double best_lang_q = 0.0;
    171 
    172     mime = MHD_lookup_connection_value (conn,
    173                                         MHD_HEADER_KIND,
    174                                         MHD_HTTP_HEADER_ACCEPT);
    175     if (NULL == mime)
    176       mime = "text/plain";
    177     lang = MHD_lookup_connection_value (conn,
    178                                         MHD_HEADER_KIND,
    179                                         MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    180     if (NULL == lang)
    181       lang = "en";
    182     /* Find best match: must match mime type (if possible), and if
    183        mime type matches, ideally also language */
    184     for (struct Terms *p = legal->terms_head;
    185          NULL != p;
    186          p = p->next)
    187     {
    188       double q;
    189 
    190       q = TALER_pattern_matches (mime,
    191                                  p->mime_type);
    192       if (q > best_mime_q)
    193         best_mime_q = q;
    194     }
    195     for (struct Terms *p = legal->terms_head;
    196          NULL != p;
    197          p = p->next)
    198     {
    199       double q;
    200 
    201       q = TALER_pattern_matches (mime,
    202                                  p->mime_type);
    203       if (q < best_mime_q)
    204         continue;
    205       q = TALER_pattern_matches (lang,
    206                                  p->language);
    207       /* create 'available-languages' (for this mime-type) */
    208       if (NULL == langs)
    209       {
    210         langs = GNUNET_strdup (p->language);
    211       }
    212       else if (NULL == strstr (langs,
    213                                p->language))
    214       {
    215         char *tmp = langs;
    216 
    217         GNUNET_asprintf (&langs,
    218                          "%s,%s",
    219                          tmp,
    220                          p->language);
    221         GNUNET_free (tmp);
    222       }
    223       if (q < best_lang_q)
    224         continue;
    225       best_lang_q = q;
    226       t = p;
    227     }
    228     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    229                 "Best match for %s/%s: %s / %s\n",
    230                 lang,
    231                 mime,
    232                 (NULL != t) ? t->mime_type : "<none>",
    233                 (NULL != t) ? t->language : "<none>");
    234   }
    235 
    236   if (NULL != t)
    237   {
    238     const char *etag;
    239 
    240     etag = MHD_lookup_connection_value (conn,
    241                                         MHD_HEADER_KIND,
    242                                         MHD_HTTP_HEADER_IF_NONE_MATCH);
    243     if ( (NULL != etag) &&
    244          (NULL != t->terms_etag) &&
    245          (0 == strcasecmp (etag,
    246                            t->terms_etag)) )
    247     {
    248       MHD_RESULT ret;
    249 
    250       resp = MHD_create_response_from_buffer (0,
    251                                               NULL,
    252                                               MHD_RESPMEM_PERSISTENT);
    253       TALER_MHD_add_global_headers (resp,
    254                                     true);
    255       GNUNET_break (MHD_YES ==
    256                     MHD_add_response_header (resp,
    257                                              MHD_HTTP_HEADER_EXPIRES,
    258                                              dat));
    259       GNUNET_break (MHD_YES ==
    260                     MHD_add_response_header (resp,
    261                                              MHD_HTTP_HEADER_ETAG,
    262                                              t->terms_etag));
    263       if (NULL != legal)
    264         GNUNET_break (MHD_YES ==
    265                       MHD_add_response_header (resp,
    266                                                TALER_TERMS_VERSION,
    267                                                legal->terms_version));
    268       ret = MHD_queue_response (conn,
    269                                 MHD_HTTP_NOT_MODIFIED,
    270                                 resp);
    271       GNUNET_break (MHD_YES == ret);
    272       MHD_destroy_response (resp);
    273       return ret;
    274     }
    275   }
    276 
    277   if (NULL == t)
    278     t = &none; /* 501 if not configured */
    279 
    280 return_t:
    281   /* try to compress the response */
    282   resp = NULL;
    283   if ( (TALER_MHD_CT_DEFLATE ==
    284         TALER_MHD_can_compress (conn,
    285                                 TALER_MHD_CT_DEFLATE)) &&
    286        (NULL != t->compressed_terms) )
    287   {
    288     resp = MHD_create_response_from_buffer (t->compressed_terms_size,
    289                                             t->compressed_terms,
    290                                             MHD_RESPMEM_PERSISTENT);
    291     if (MHD_NO ==
    292         MHD_add_response_header (resp,
    293                                  MHD_HTTP_HEADER_CONTENT_ENCODING,
    294                                  "deflate"))
    295     {
    296       GNUNET_break (0);
    297       MHD_destroy_response (resp);
    298       resp = NULL;
    299     }
    300   }
    301   if (NULL == resp)
    302   {
    303     /* could not generate compressed response, return uncompressed */
    304     resp = MHD_create_response_from_buffer (t->terms_size,
    305                                             (void *) t->terms,
    306                                             MHD_RESPMEM_PERSISTENT);
    307   }
    308   TALER_MHD_add_global_headers (resp,
    309                                 true);
    310   GNUNET_break (MHD_YES ==
    311                 MHD_add_response_header (resp,
    312                                          MHD_HTTP_HEADER_EXPIRES,
    313                                          dat));
    314   if (NULL != langs)
    315   {
    316     GNUNET_break (MHD_YES ==
    317                   MHD_add_response_header (resp,
    318                                            "Avail-Languages",
    319                                            langs));
    320     GNUNET_free (langs);
    321   }
    322   /* Set cache control headers: our response varies depending on these headers */
    323   GNUNET_break (MHD_YES ==
    324                 MHD_add_response_header (resp,
    325                                          MHD_HTTP_HEADER_VARY,
    326                                          MHD_HTTP_HEADER_ACCEPT_LANGUAGE ","
    327                                          MHD_HTTP_HEADER_ACCEPT ","
    328                                          MHD_HTTP_HEADER_ACCEPT_ENCODING));
    329   /* Information is always public, revalidate after 10 days */
    330   GNUNET_break (MHD_YES ==
    331                 MHD_add_response_header (resp,
    332                                          MHD_HTTP_HEADER_CACHE_CONTROL,
    333                                          "public,max-age=864000"));
    334   if (NULL != t->terms_etag)
    335     GNUNET_break (MHD_YES ==
    336                   MHD_add_response_header (resp,
    337                                            MHD_HTTP_HEADER_ETAG,
    338                                            t->terms_etag));
    339   if (NULL != legal)
    340     GNUNET_break (MHD_YES ==
    341                   MHD_add_response_header (resp,
    342                                            TALER_TERMS_VERSION,
    343                                            legal->terms_version));
    344   GNUNET_break (MHD_YES ==
    345                 MHD_add_response_header (resp,
    346                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    347                                          t->mime_type));
    348   GNUNET_break (MHD_YES ==
    349                 MHD_add_response_header (resp,
    350                                          MHD_HTTP_HEADER_CONTENT_LANGUAGE,
    351                                          t->language));
    352   {
    353     MHD_RESULT ret;
    354 
    355     ret = MHD_queue_response (conn,
    356                               t == &none
    357                               ? MHD_HTTP_NOT_IMPLEMENTED
    358                               : MHD_HTTP_OK,
    359                               resp);
    360     MHD_destroy_response (resp);
    361     return ret;
    362   }
    363 }
    364 
    365 
    366 /**
    367  * Load all the terms of service from @a path under language @a lang
    368  * from file @a name
    369  *
    370  * @param[in,out] legal where to write the result
    371  * @param path where the terms are found
    372  * @param lang which language directory to crawl
    373  * @param name specific file to access
    374  */
    375 static void
    376 load_terms (struct TALER_MHD_Legal *legal,
    377             const char *path,
    378             const char *lang,
    379             const char *name)
    380 {
    381   static struct MimeMap
    382   {
    383     const char *ext;
    384     const char *mime;
    385     unsigned int priority;
    386   } mm[] = {
    387     { .ext = ".txt", .mime = "text/plain", .priority = 150 },
    388     { .ext = ".html", .mime = "text/html", .priority = 100 },
    389     { .ext = ".htm", .mime = "text/html", .priority = 99 },
    390     { .ext = ".md", .mime = "text/markdown", .priority = 50 },
    391     { .ext = ".pdf", .mime = "application/pdf", .priority = 25 },
    392     { .ext = ".jpg", .mime = "image/jpeg" },
    393     { .ext = ".jpeg", .mime = "image/jpeg" },
    394     { .ext = ".png", .mime = "image/png" },
    395     { .ext = ".gif", .mime = "image/gif" },
    396     { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 },
    397     { .ext = ".xml", .mime = "text/xml", .priority = 10 },
    398     { .ext = NULL, .mime = NULL }
    399   };
    400   const char *ext = strrchr (name, '.');
    401   const char *mime;
    402   unsigned int priority;
    403 
    404   if (NULL == ext)
    405   {
    406     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    407                 "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
    408                 name,
    409                 path,
    410                 lang);
    411     return;
    412   }
    413   if ( (NULL == legal->terms_version) ||
    414        (0 != strncmp (legal->terms_version,
    415                       name,
    416                       ext - name - 1)) )
    417   {
    418     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    419                 "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n",
    420                 name,
    421                 legal->terms_version,
    422                 path,
    423                 lang);
    424     return;
    425   }
    426   mime = NULL;
    427   for (unsigned int i = 0; NULL != mm[i].ext; i++)
    428     if (0 == strcasecmp (mm[i].ext,
    429                          ext))
    430     {
    431       mime = mm[i].mime;
    432       priority = mm[i].priority;
    433       break;
    434     }
    435   if (NULL == mime)
    436   {
    437     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    438                 "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n",
    439                 ext,
    440                 name,
    441                 path,
    442                 lang);
    443     return;
    444   }
    445   /* try to read the file with the terms of service */
    446   {
    447     struct stat st;
    448     char *fn;
    449     int fd;
    450 
    451     GNUNET_asprintf (&fn,
    452                      "%s/%s/%s",
    453                      path,
    454                      lang,
    455                      name);
    456     fd = open (fn, O_RDONLY);
    457     if (-1 == fd)
    458     {
    459       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    460                                 "open",
    461                                 fn);
    462       GNUNET_free (fn);
    463       return;
    464     }
    465     if (0 != fstat (fd, &st))
    466     {
    467       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    468                                 "fstat",
    469                                 fn);
    470       GNUNET_break (0 == close (fd));
    471       GNUNET_free (fn);
    472       return;
    473     }
    474     if (SIZE_MAX < ((unsigned long long) st.st_size))
    475     {
    476       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    477                                 "fstat-size",
    478                                 fn);
    479       GNUNET_break (0 == close (fd));
    480       GNUNET_free (fn);
    481       return;
    482     }
    483     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    484                 "Loading legal information from file `%s'\n",
    485                 fn);
    486     {
    487       void *buf;
    488       size_t bsize;
    489 
    490       bsize = (size_t) st.st_size;
    491       buf = mmap (NULL,
    492                   bsize,
    493                   PROT_READ,
    494                   MAP_SHARED,
    495                   fd,
    496                   0);
    497       if (MAP_FAILED == buf)
    498       {
    499         GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    500                                   "mmap",
    501                                   fn);
    502         GNUNET_break (0 == close (fd));
    503         GNUNET_free (fn);
    504         return;
    505       }
    506       GNUNET_break (0 == close (fd));
    507       GNUNET_free (fn);
    508 
    509       /* insert into global list of terms of service */
    510       {
    511         struct Terms *t;
    512         struct GNUNET_HashCode hc;
    513 
    514         GNUNET_CRYPTO_hash (buf,
    515                             bsize,
    516                             &hc);
    517         t = GNUNET_new (struct Terms);
    518         t->mime_type = mime;
    519         t->terms = buf;
    520         t->language = GNUNET_strdup (lang);
    521         t->terms_size = bsize;
    522         t->priority = priority;
    523         t->terms_etag
    524           = GNUNET_STRINGS_data_to_string_alloc (&hc,
    525                                                  sizeof (hc) / 2);
    526         buf = GNUNET_memdup (t->terms,
    527                              t->terms_size);
    528         if (TALER_MHD_body_compress (&buf,
    529                                      &bsize))
    530         {
    531           t->compressed_terms = buf;
    532           t->compressed_terms_size = bsize;
    533         }
    534         else
    535         {
    536           GNUNET_free (buf);
    537         }
    538         {
    539           struct Terms *prev = NULL;
    540 
    541           for (struct Terms *pos = legal->terms_head;
    542                NULL != pos;
    543                pos = pos->next)
    544           {
    545             if (pos->priority < priority)
    546               break;
    547             prev = pos;
    548           }
    549           GNUNET_CONTAINER_DLL_insert_after (legal->terms_head,
    550                                              legal->terms_tail,
    551                                              prev,
    552                                              t);
    553         }
    554       }
    555     }
    556   }
    557 }
    558 
    559 
    560 /**
    561  * Load all the terms of service from @a path under language @a lang.
    562  *
    563  * @param[in,out] legal where to write the result
    564  * @param path where the terms are found
    565  * @param lang which language directory to crawl
    566  */
    567 static void
    568 load_language (struct TALER_MHD_Legal *legal,
    569                const char *path,
    570                const char *lang)
    571 {
    572   char *dname;
    573   DIR *d;
    574 
    575   GNUNET_asprintf (&dname,
    576                    "%s/%s",
    577                    path,
    578                    lang);
    579   d = opendir (dname);
    580   if (NULL == d)
    581   {
    582     GNUNET_free (dname);
    583     return;
    584   }
    585   for (struct dirent *de = readdir (d);
    586        NULL != de;
    587        de = readdir (d))
    588   {
    589     const char *fn = de->d_name;
    590 
    591     if (fn[0] == '.')
    592       continue;
    593     load_terms (legal,
    594                 path,
    595                 lang,
    596                 fn);
    597   }
    598   GNUNET_break (0 == closedir (d));
    599   GNUNET_free (dname);
    600 }
    601 
    602 
    603 struct TALER_MHD_Legal *
    604 TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
    605                       const char *section,
    606                       const char *diroption,
    607                       const char *tagoption)
    608 {
    609   struct TALER_MHD_Legal *legal;
    610   char *path;
    611   DIR *d;
    612 
    613   legal = GNUNET_new (struct TALER_MHD_Legal);
    614   if (GNUNET_OK !=
    615       GNUNET_CONFIGURATION_get_value_string (cfg,
    616                                              section,
    617                                              tagoption,
    618                                              &legal->terms_version))
    619   {
    620     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
    621                                section,
    622                                tagoption);
    623     GNUNET_free (legal);
    624     return NULL;
    625   }
    626   if (GNUNET_OK !=
    627       GNUNET_CONFIGURATION_get_value_filename (cfg,
    628                                                section,
    629                                                diroption,
    630                                                &path))
    631   {
    632     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
    633                                section,
    634                                diroption);
    635     GNUNET_free (legal->terms_version);
    636     GNUNET_free (legal);
    637     return NULL;
    638   }
    639   d = opendir (path);
    640   if (NULL == d)
    641   {
    642     GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
    643                                section,
    644                                diroption,
    645                                "Could not open directory");
    646     GNUNET_free (legal->terms_version);
    647     GNUNET_free (legal);
    648     GNUNET_free (path);
    649     return NULL;
    650   }
    651   for (struct dirent *de = readdir (d);
    652        NULL != de;
    653        de = readdir (d))
    654   {
    655     const char *lang = de->d_name;
    656 
    657     if (lang[0] == '.')
    658       continue;
    659     if (0 == strcmp (lang,
    660                      "locale"))
    661       continue;
    662     load_language (legal,
    663                    path,
    664                    lang);
    665   }
    666   GNUNET_break (0 == closedir (d));
    667   GNUNET_free (path);
    668   return legal;
    669 }
    670 
    671 
    672 void
    673 TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
    674 {
    675   struct Terms *t;
    676   if (NULL == legal)
    677     return;
    678   while (NULL != (t = legal->terms_head))
    679   {
    680     GNUNET_free (t->language);
    681     GNUNET_free (t->compressed_terms);
    682     if (0 != munmap (t->terms, t->terms_size))
    683       GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
    684                            "munmap");
    685     GNUNET_CONTAINER_DLL_remove (legal->terms_head,
    686                                  legal->terms_tail,
    687                                  t);
    688     GNUNET_free (t->terms_etag);
    689     GNUNET_free (t);
    690   }
    691   GNUNET_free (legal->terms_version);
    692   GNUNET_free (legal);
    693 }