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


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