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 (21093B)


      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 "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 /**
    125  * Check if the comma-separated list @a langs already contains
    126  * the entry @a lang.
    127  *
    128  * @param langs comma-separated list of languages
    129  * @param lang language to look for
    130  * @return true if @a lang is an entry of @a langs
    131  */
    132 static bool
    133 lang_in_list (const char *langs,
    134               const char *lang)
    135 {
    136   size_t len = strlen (lang);
    137   const char *pos = langs;
    138 
    139   while (NULL != pos)
    140   {
    141     const char *end = strchr (pos,
    142                               ',');
    143     size_t elen = (NULL == end)
    144                   ? strlen (pos)
    145                   : (size_t) (end - pos);
    146 
    147     if ( (elen == len) &&
    148          (0 == strncasecmp (pos,
    149                             lang,
    150                             len)) )
    151       return true;
    152     pos = (NULL == end)
    153           ? NULL
    154           : end + 1;
    155   }
    156   return false;
    157 }
    158 
    159 
    160 enum MHD_Result
    161 TALER_MHD_reply_legal (struct MHD_Connection *conn,
    162                        struct TALER_MHD_Legal *legal)
    163 {
    164   /* Default terms of service if none are configured */
    165   static struct Terms none = {
    166     .mime_type = "text/plain",
    167     .terms = (void *) "not configured",
    168     .language = (void *) "en",
    169     .terms_size = strlen ("not configured")
    170   };
    171   struct MHD_Response *resp;
    172   struct Terms *t;
    173   struct GNUNET_TIME_Absolute a;
    174   struct GNUNET_TIME_Timestamp m;
    175   char dat[128];
    176   char *langs;
    177 
    178   t = NULL;
    179   langs = NULL;
    180 
    181   a = GNUNET_TIME_relative_to_absolute (MAX_TERMS_CACHING);
    182   /* Round up to next full day to ensure the expiration
    183      time does not become a fingerprint! */
    184   a = GNUNET_TIME_absolute_round_down (a,
    185                                        MAX_TERMS_CACHING);
    186   a = GNUNET_TIME_absolute_add (a,
    187                                 MAX_TERMS_CACHING);
    188   m = GNUNET_TIME_absolute_to_timestamp (a);
    189   TALER_MHD_get_date_string (m.abs_time,
    190                              dat);
    191   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    192               "Setting '%s' header to '%s'\n",
    193               MHD_HTTP_HEADER_EXPIRES,
    194               dat);
    195   if (NULL == legal)
    196   {
    197     t = &none;
    198     goto return_t;
    199   }
    200 
    201   if (NULL != legal)
    202   {
    203     const char *mime;
    204     const char *lang;
    205     double best_mime_q = 0.0;
    206     double best_lang_q = 0.0;
    207 
    208     mime = MHD_lookup_connection_value (conn,
    209                                         MHD_HEADER_KIND,
    210                                         MHD_HTTP_HEADER_ACCEPT);
    211     if (NULL == mime)
    212       mime = "text/plain";
    213     lang = MHD_lookup_connection_value (conn,
    214                                         MHD_HEADER_KIND,
    215                                         MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
    216     if (NULL == lang)
    217       lang = "en";
    218     /* Find best match: must match mime type (if possible), and if
    219        mime type matches, ideally also language */
    220     for (struct Terms *p = legal->terms_head;
    221          NULL != p;
    222          p = p->next)
    223     {
    224       double q;
    225 
    226       q = TALER_pattern_matches (mime,
    227                                  p->mime_type);
    228       if (q > best_mime_q)
    229         best_mime_q = q;
    230     }
    231     for (struct Terms *p = legal->terms_head;
    232          NULL != p;
    233          p = p->next)
    234     {
    235       double q;
    236 
    237       q = TALER_pattern_matches (mime,
    238                                  p->mime_type);
    239       if (q < best_mime_q)
    240         continue;
    241       q = TALER_pattern_matches (lang,
    242                                  p->language);
    243       /* create 'available-languages' (for this mime-type) */
    244       if (NULL == langs)
    245       {
    246         langs = GNUNET_strdup (p->language);
    247       }
    248       else if (! lang_in_list (langs,
    249                                p->language))
    250       {
    251         char *tmp = langs;
    252 
    253         GNUNET_asprintf (&langs,
    254                          "%s,%s",
    255                          tmp,
    256                          p->language);
    257         GNUNET_free (tmp);
    258       }
    259       if (NULL != t)
    260       {
    261         if (q < best_lang_q)
    262           continue;
    263         /* Client expressed no preference between these two formats,
    264            so fall back to our own ranking instead of letting the
    265            last entry in the list win. */
    266         if ( (q <= best_lang_q) &&
    267              (p->priority <= t->priority) )
    268           continue;
    269       }
    270       best_lang_q = q;
    271       t = p;
    272     }
    273     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    274                 "Best match for %s/%s: %s / %s\n",
    275                 lang,
    276                 mime,
    277                 (NULL != t) ? t->mime_type : "<none>",
    278                 (NULL != t) ? t->language : "<none>");
    279   }
    280 
    281   if (NULL != t)
    282   {
    283     const char *etag;
    284 
    285     etag = MHD_lookup_connection_value (conn,
    286                                         MHD_HEADER_KIND,
    287                                         MHD_HTTP_HEADER_IF_NONE_MATCH);
    288     if ( (NULL != etag) &&
    289          (NULL != t->terms_etag) &&
    290          (0 == strcasecmp (etag,
    291                            t->terms_etag)) )
    292     {
    293       enum MHD_Result ret;
    294 
    295       resp = MHD_create_response_from_buffer (0,
    296                                               NULL,
    297                                               MHD_RESPMEM_PERSISTENT);
    298       TALER_MHD_add_global_headers (resp,
    299                                     true);
    300       GNUNET_break (MHD_YES ==
    301                     MHD_add_response_header (resp,
    302                                              MHD_HTTP_HEADER_EXPIRES,
    303                                              dat));
    304       GNUNET_break (MHD_YES ==
    305                     MHD_add_response_header (resp,
    306                                              MHD_HTTP_HEADER_ETAG,
    307                                              t->terms_etag));
    308       if (NULL != legal)
    309         GNUNET_break (MHD_YES ==
    310                       MHD_add_response_header (resp,
    311                                                TALER_TERMS_VERSION,
    312                                                legal->terms_version));
    313       ret = MHD_queue_response (conn,
    314                                 MHD_HTTP_NOT_MODIFIED,
    315                                 resp);
    316       GNUNET_break (MHD_YES == ret);
    317       MHD_destroy_response (resp);
    318       GNUNET_free (langs);
    319       return ret;
    320     }
    321   }
    322 
    323   if (NULL == t)
    324     t = &none; /* 501 if not configured */
    325 
    326 return_t:
    327   /* try to compress the response */
    328   resp = NULL;
    329   if ( (TALER_MHD_CT_DEFLATE ==
    330         TALER_MHD_can_compress (conn,
    331                                 TALER_MHD_CT_DEFLATE)) &&
    332        (NULL != t->compressed_terms) )
    333   {
    334     resp = MHD_create_response_from_buffer (t->compressed_terms_size,
    335                                             t->compressed_terms,
    336                                             MHD_RESPMEM_PERSISTENT);
    337     if (MHD_NO ==
    338         MHD_add_response_header (resp,
    339                                  MHD_HTTP_HEADER_CONTENT_ENCODING,
    340                                  "deflate"))
    341     {
    342       GNUNET_break (0);
    343       MHD_destroy_response (resp);
    344       resp = NULL;
    345     }
    346   }
    347   if (NULL == resp)
    348   {
    349     /* could not generate compressed response, return uncompressed */
    350     resp = MHD_create_response_from_buffer (t->terms_size,
    351                                             (void *) t->terms,
    352                                             MHD_RESPMEM_PERSISTENT);
    353   }
    354   TALER_MHD_add_global_headers (resp,
    355                                 true);
    356   GNUNET_break (MHD_YES ==
    357                 MHD_add_response_header (resp,
    358                                          MHD_HTTP_HEADER_EXPIRES,
    359                                          dat));
    360   if (NULL != langs)
    361   {
    362     GNUNET_break (MHD_YES ==
    363                   MHD_add_response_header (resp,
    364                                            "Avail-Languages",
    365                                            langs));
    366     GNUNET_free (langs);
    367   }
    368   /* Set cache control headers: our response varies depending on these headers */
    369   GNUNET_break (MHD_YES ==
    370                 MHD_add_response_header (resp,
    371                                          MHD_HTTP_HEADER_VARY,
    372                                          MHD_HTTP_HEADER_ACCEPT_LANGUAGE ","
    373                                          MHD_HTTP_HEADER_ACCEPT ","
    374                                          MHD_HTTP_HEADER_ACCEPT_ENCODING));
    375   /* Information is always public, revalidate after 10 days */
    376   GNUNET_break (MHD_YES ==
    377                 MHD_add_response_header (resp,
    378                                          MHD_HTTP_HEADER_CACHE_CONTROL,
    379                                          "public,max-age=864000"));
    380   if (NULL != t->terms_etag)
    381     GNUNET_break (MHD_YES ==
    382                   MHD_add_response_header (resp,
    383                                            MHD_HTTP_HEADER_ETAG,
    384                                            t->terms_etag));
    385   if (NULL != legal)
    386     GNUNET_break (MHD_YES ==
    387                   MHD_add_response_header (resp,
    388                                            TALER_TERMS_VERSION,
    389                                            legal->terms_version));
    390   GNUNET_break (MHD_YES ==
    391                 MHD_add_response_header (resp,
    392                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    393                                          t->mime_type));
    394   GNUNET_break (MHD_YES ==
    395                 MHD_add_response_header (resp,
    396                                          MHD_HTTP_HEADER_CONTENT_LANGUAGE,
    397                                          t->language));
    398   {
    399     enum MHD_Result ret;
    400 
    401     ret = MHD_queue_response (conn,
    402                               t == &none
    403                               ? MHD_HTTP_NOT_IMPLEMENTED
    404                               : MHD_HTTP_OK,
    405                               resp);
    406     MHD_destroy_response (resp);
    407     return ret;
    408   }
    409 }
    410 
    411 
    412 /**
    413  * Load all the terms of service from @a path under language @a lang
    414  * from file @a name
    415  *
    416  * @param[in,out] legal where to write the result
    417  * @param path where the terms are found
    418  * @param lang which language directory to crawl
    419  * @param name specific file to access
    420  */
    421 static void
    422 load_terms (struct TALER_MHD_Legal *legal,
    423             const char *path,
    424             const char *lang,
    425             const char *name)
    426 {
    427   static struct MimeMap
    428   {
    429     const char *ext;
    430     const char *mime;
    431     unsigned int priority;
    432   } mm[] = {
    433     { .ext = ".txt", .mime = "text/plain", .priority = 150 },
    434     { .ext = ".html", .mime = "text/html", .priority = 100 },
    435     { .ext = ".htm", .mime = "text/html", .priority = 99 },
    436     { .ext = ".md", .mime = "text/markdown", .priority = 50 },
    437     { .ext = ".pdf", .mime = "application/pdf", .priority = 25 },
    438     { .ext = ".jpg", .mime = "image/jpeg" },
    439     { .ext = ".jpeg", .mime = "image/jpeg" },
    440     { .ext = ".png", .mime = "image/png" },
    441     { .ext = ".gif", .mime = "image/gif" },
    442     { .ext = ".epub", .mime = "application/epub+zip", .priority = 10 },
    443     { .ext = ".xml", .mime = "text/xml", .priority = 10 },
    444     { .ext = NULL, .mime = NULL }
    445   };
    446   const char *ext = strrchr (name, '.');
    447   const char *mime;
    448   unsigned int priority;
    449 
    450   if (NULL == ext)
    451   {
    452     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    453                 "Unsupported file `%s' in directory `%s/%s': lacks extension\n",
    454                 name,
    455                 path,
    456                 lang);
    457     return;
    458   }
    459   if ( (NULL == legal->terms_version) ||
    460        (0 != strncmp (legal->terms_version,
    461                       name,
    462                       ext - name)) )
    463   {
    464     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    465                 "Filename `%s' does not match Etag `%s' in directory `%s/%s'. Ignoring it.\n",
    466                 name,
    467                 legal->terms_version,
    468                 path,
    469                 lang);
    470     return;
    471   }
    472   mime = NULL;
    473   for (unsigned int i = 0; NULL != mm[i].ext; i++)
    474     if (0 == strcasecmp (mm[i].ext,
    475                          ext))
    476     {
    477       mime = mm[i].mime;
    478       priority = mm[i].priority;
    479       break;
    480     }
    481   if (NULL == mime)
    482   {
    483     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    484                 "Unsupported file extension `%s' of file `%s' in directory `%s/%s'\n",
    485                 ext,
    486                 name,
    487                 path,
    488                 lang);
    489     return;
    490   }
    491   /* try to read the file with the terms of service */
    492   {
    493     struct stat st;
    494     char *fn;
    495     int fd;
    496 
    497     GNUNET_asprintf (&fn,
    498                      "%s/%s/%s",
    499                      path,
    500                      lang,
    501                      name);
    502     fd = open (fn, O_RDONLY);
    503     if (-1 == fd)
    504     {
    505       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    506                                 "open",
    507                                 fn);
    508       GNUNET_free (fn);
    509       return;
    510     }
    511     if (0 != fstat (fd, &st))
    512     {
    513       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    514                                 "fstat",
    515                                 fn);
    516       GNUNET_break (0 == close (fd));
    517       GNUNET_free (fn);
    518       return;
    519     }
    520     if (SIZE_MAX < ((unsigned long long) st.st_size))
    521     {
    522       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    523                                 "fstat-size",
    524                                 fn);
    525       GNUNET_break (0 == close (fd));
    526       GNUNET_free (fn);
    527       return;
    528     }
    529     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    530                 "Loading legal information from file `%s'\n",
    531                 fn);
    532     {
    533       void *buf;
    534       size_t bsize;
    535 
    536       bsize = (size_t) st.st_size;
    537       buf = mmap (NULL,
    538                   bsize,
    539                   PROT_READ,
    540                   MAP_SHARED,
    541                   fd,
    542                   0);
    543       if (MAP_FAILED == buf)
    544       {
    545         GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    546                                   "mmap",
    547                                   fn);
    548         GNUNET_break (0 == close (fd));
    549         GNUNET_free (fn);
    550         return;
    551       }
    552       GNUNET_break (0 == close (fd));
    553       GNUNET_free (fn);
    554 
    555       /* insert into global list of terms of service */
    556       {
    557         struct Terms *t;
    558         struct GNUNET_HashCode hc;
    559 
    560         GNUNET_CRYPTO_hash (buf,
    561                             bsize,
    562                             &hc);
    563         t = GNUNET_new (struct Terms);
    564         t->mime_type = mime;
    565         t->terms = buf;
    566         t->language = GNUNET_strdup (lang);
    567         t->terms_size = bsize;
    568         t->priority = priority;
    569         t->terms_etag
    570           = GNUNET_STRINGS_data_to_string_alloc (&hc,
    571                                                  sizeof (hc) / 2);
    572         buf = GNUNET_memdup (t->terms,
    573                              t->terms_size);
    574         if (TALER_MHD_body_compress (&buf,
    575                                      &bsize))
    576         {
    577           t->compressed_terms = buf;
    578           t->compressed_terms_size = bsize;
    579         }
    580         else
    581         {
    582           GNUNET_free (buf);
    583         }
    584         {
    585           struct Terms *prev = NULL;
    586 
    587           for (struct Terms *pos = legal->terms_head;
    588                NULL != pos;
    589                pos = pos->next)
    590           {
    591             if (pos->priority < priority)
    592               break;
    593             prev = pos;
    594           }
    595           GNUNET_CONTAINER_DLL_insert_after (legal->terms_head,
    596                                              legal->terms_tail,
    597                                              prev,
    598                                              t);
    599         }
    600       }
    601     }
    602   }
    603 }
    604 
    605 
    606 /**
    607  * Load all the terms of service from @a path under language @a lang.
    608  *
    609  * @param[in,out] legal where to write the result
    610  * @param path where the terms are found
    611  * @param lang which language directory to crawl
    612  */
    613 static void
    614 load_language (struct TALER_MHD_Legal *legal,
    615                const char *path,
    616                const char *lang)
    617 {
    618   char *dname;
    619   DIR *d;
    620 
    621   GNUNET_asprintf (&dname,
    622                    "%s/%s",
    623                    path,
    624                    lang);
    625   d = opendir (dname);
    626   if (NULL == d)
    627   {
    628     GNUNET_free (dname);
    629     return;
    630   }
    631   for (struct dirent *de = readdir (d);
    632        NULL != de;
    633        de = readdir (d))
    634   {
    635     const char *fn = de->d_name;
    636 
    637     if (fn[0] == '.')
    638       continue;
    639     load_terms (legal,
    640                 path,
    641                 lang,
    642                 fn);
    643   }
    644   GNUNET_break (0 == closedir (d));
    645   GNUNET_free (dname);
    646 }
    647 
    648 
    649 struct TALER_MHD_Legal *
    650 TALER_MHD_legal_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
    651                       const char *section,
    652                       const char *diroption,
    653                       const char *tagoption)
    654 {
    655   struct TALER_MHD_Legal *legal;
    656   char *path;
    657   DIR *d;
    658 
    659   legal = GNUNET_new (struct TALER_MHD_Legal);
    660   if (GNUNET_OK !=
    661       GNUNET_CONFIGURATION_get_value_string (cfg,
    662                                              section,
    663                                              tagoption,
    664                                              &legal->terms_version))
    665   {
    666     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
    667                                section,
    668                                tagoption);
    669     GNUNET_free (legal);
    670     return NULL;
    671   }
    672   if (GNUNET_OK !=
    673       GNUNET_CONFIGURATION_get_value_filename (cfg,
    674                                                section,
    675                                                diroption,
    676                                                &path))
    677   {
    678     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
    679                                section,
    680                                diroption);
    681     GNUNET_free (legal->terms_version);
    682     GNUNET_free (legal);
    683     return NULL;
    684   }
    685   d = opendir (path);
    686   if (NULL == d)
    687   {
    688     GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
    689                                section,
    690                                diroption,
    691                                "Could not open directory");
    692     GNUNET_free (legal->terms_version);
    693     GNUNET_free (legal);
    694     GNUNET_free (path);
    695     return NULL;
    696   }
    697   for (struct dirent *de = readdir (d);
    698        NULL != de;
    699        de = readdir (d))
    700   {
    701     const char *lang = de->d_name;
    702 
    703     if (lang[0] == '.')
    704       continue;
    705     if (0 == strcmp (lang,
    706                      "locale"))
    707       continue;
    708     load_language (legal,
    709                    path,
    710                    lang);
    711   }
    712   GNUNET_break (0 == closedir (d));
    713   GNUNET_free (path);
    714   return legal;
    715 }
    716 
    717 
    718 void
    719 TALER_MHD_legal_free (struct TALER_MHD_Legal *legal)
    720 {
    721   struct Terms *t;
    722   if (NULL == legal)
    723     return;
    724   while (NULL != (t = legal->terms_head))
    725   {
    726     GNUNET_free (t->language);
    727     GNUNET_free (t->compressed_terms);
    728     if (0 != munmap (t->terms, t->terms_size))
    729       GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
    730                            "munmap");
    731     GNUNET_CONTAINER_DLL_remove (legal->terms_head,
    732                                  legal->terms_tail,
    733                                  t);
    734     GNUNET_free (t->terms_etag);
    735     GNUNET_free (t);
    736   }
    737   GNUNET_free (legal->terms_version);
    738   GNUNET_free (legal);
    739 }