exchange

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

mhd2_legal.c (20459B)


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