exchange

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

test_legal.c (13419B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 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/test_legal.c
     18  * @brief Tests for content negotiation on legal documents
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include <microhttpd.h>
     24 #include <curl/curl.h>
     25 #include "taler/taler_util.h"
     26 #include "taler/taler_mhd_lib.h"
     27 
     28 /**
     29  * Version (Etag) of the terms we serve in this test.
     30  */
     31 #define TERMS_VERSION "test-tos-v0"
     32 
     33 /**
     34  * Terms of service we serve during the test.
     35  */
     36 static struct TALER_MHD_Legal *legal;
     37 
     38 /**
     39  * Directory with the terms of service.
     40  */
     41 static char *tmpdir;
     42 
     43 /**
     44  * Return value of the test.
     45  */
     46 static int global_ret;
     47 
     48 
     49 /**
     50  * Response headers we care about.
     51  */
     52 struct Headers
     53 {
     54   /**
     55    * Value of the "Content-Type" header, or empty.
     56    */
     57   char content_type[128];
     58 
     59   /**
     60    * Value of the "Content-Language" header, or empty.
     61    */
     62   char content_language[128];
     63 };
     64 
     65 
     66 /**
     67  * Extract @a name from the header line in @a buf of @a len bytes
     68  * into @a dst of @a dst_size bytes.
     69  */
     70 static void
     71 extract_header (const char *buf,
     72                 size_t len,
     73                 const char *name,
     74                 char *dst,
     75                 size_t dst_size)
     76 {
     77   size_t nlen = strlen (name);
     78   size_t vlen;
     79 
     80   if ( (len < nlen + 1) ||
     81        (0 != strncasecmp (buf,
     82                           name,
     83                           nlen)) ||
     84        (':' != buf[nlen]) )
     85     return;
     86   buf += nlen + 1;
     87   len -= nlen + 1;
     88   while ( (0 != len) &&
     89           (isspace ((int) *buf)) )
     90   {
     91     buf++;
     92     len--;
     93   }
     94   while ( (0 != len) &&
     95           (isspace ((int) buf[len - 1])) )
     96     len--;
     97   vlen = GNUNET_MIN (len,
     98                      dst_size - 1);
     99   memcpy (dst,
    100           buf,
    101           vlen);
    102   dst[vlen] = '\0';
    103 }
    104 
    105 
    106 /**
    107  * Function called by CURL for each response header.
    108  */
    109 static size_t
    110 header_cb (char *buffer,
    111            size_t size,
    112            size_t nitems,
    113            void *userdata)
    114 {
    115   struct Headers *h = userdata;
    116   size_t len = size * nitems;
    117 
    118   extract_header (buffer,
    119                   len,
    120                   "Content-Type",
    121                   h->content_type,
    122                   sizeof (h->content_type));
    123   extract_header (buffer,
    124                   len,
    125                   "Content-Language",
    126                   h->content_language,
    127                   sizeof (h->content_language));
    128   return len;
    129 }
    130 
    131 
    132 /**
    133  * Function called by CURL with the response body; we discard it.
    134  */
    135 static size_t
    136 body_cb (char *buffer,
    137          size_t size,
    138          size_t nitems,
    139          void *userdata)
    140 {
    141   (void) buffer;
    142   (void) userdata;
    143   return size * nitems;
    144 }
    145 
    146 
    147 /**
    148  * Handle a request by returning our terms of service.
    149  */
    150 static enum MHD_Result
    151 access_handler (void *cls,
    152                 struct MHD_Connection *connection,
    153                 const char *url,
    154                 const char *method,
    155                 const char *version,
    156                 const char *upload_data,
    157                 size_t *upload_data_size,
    158                 void **con_cls)
    159 {
    160   static int marker;
    161 
    162   (void) cls;
    163   (void) url;
    164   (void) method;
    165   (void) version;
    166   (void) upload_data;
    167   (void) upload_data_size;
    168   if (NULL == *con_cls)
    169   {
    170     *con_cls = &marker;
    171     return MHD_YES;
    172   }
    173   return TALER_MHD_reply_legal (connection,
    174                                 legal);
    175 }
    176 
    177 
    178 /**
    179  * Write @a content to the file @a tmpdir/@a lang/@a name.
    180  */
    181 static enum GNUNET_GenericReturnValue
    182 write_terms (const char *lang,
    183              const char *name,
    184              const char *content)
    185 {
    186   char *dn;
    187   char *fn;
    188   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
    189 
    190   GNUNET_asprintf (&dn,
    191                    "%s/%s",
    192                    tmpdir,
    193                    lang);
    194   GNUNET_asprintf (&fn,
    195                    "%s/%s",
    196                    dn,
    197                    name);
    198   if (GNUNET_OK ==
    199       GNUNET_DISK_directory_create (dn))
    200     ret = (GNUNET_SYSERR ==
    201            GNUNET_DISK_fn_write (fn,
    202                                  content,
    203                                  strlen (content),
    204                                  GNUNET_DISK_PERM_USER_READ
    205                                  | GNUNET_DISK_PERM_USER_WRITE))
    206           ? GNUNET_SYSERR
    207           : GNUNET_OK;
    208   GNUNET_free (fn);
    209   GNUNET_free (dn);
    210   return ret;
    211 }
    212 
    213 
    214 /**
    215  * Fetch the terms of service from the daemon at @a port using
    216  * @a accept and @a accept_language, and check that the response
    217  * carries @a want_type and @a want_language.
    218  *
    219  * @param port port the daemon listens on
    220  * @param accept value for the "Accept" header, NULL to send none
    221  * @param accept_language value for "Accept-Language", NULL to send none
    222  * @param want_type expected "Content-Type"
    223  * @param want_language expected "Content-Language"
    224  */
    225 static enum GNUNET_GenericReturnValue
    226 check_negotiation (uint16_t port,
    227                    const char *accept,
    228                    const char *accept_language,
    229                    const char *want_type,
    230                    const char *want_language)
    231 {
    232   CURL *eh;
    233   CURLcode res;
    234   struct curl_slist *hdrs = NULL;
    235   struct Headers h = { 0 };
    236   char *url;
    237   char *hdr;
    238   long code;
    239 
    240   GNUNET_asprintf (&url,
    241                    "http://localhost:%u/terms",
    242                    (unsigned int) port);
    243   eh = curl_easy_init ();
    244   GNUNET_assert (NULL != eh);
    245   /* CURL always sends a default wildcard "Accept" header;
    246      passing the header name with an empty value drops it. */
    247   GNUNET_asprintf (&hdr,
    248                    "Accept:%s%s",
    249                    (NULL != accept) ? " " : "",
    250                    (NULL != accept) ? accept : "");
    251   hdrs = curl_slist_append (hdrs,
    252                             hdr);
    253   GNUNET_free (hdr);
    254   if (NULL != accept_language)
    255   {
    256     GNUNET_asprintf (&hdr,
    257                      "Accept-Language: %s",
    258                      accept_language);
    259     hdrs = curl_slist_append (hdrs,
    260                               hdr);
    261     GNUNET_free (hdr);
    262   }
    263   GNUNET_assert (CURLE_OK ==
    264                  curl_easy_setopt (eh, CURLOPT_URL, url));
    265   GNUNET_assert (CURLE_OK ==
    266                  curl_easy_setopt (eh, CURLOPT_HTTPHEADER, hdrs));
    267   GNUNET_assert (CURLE_OK ==
    268                  curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &header_cb));
    269   GNUNET_assert (CURLE_OK ==
    270                  curl_easy_setopt (eh, CURLOPT_HEADERDATA, &h));
    271   GNUNET_assert (CURLE_OK ==
    272                  curl_easy_setopt (eh, CURLOPT_WRITEFUNCTION, &body_cb));
    273   GNUNET_assert (CURLE_OK ==
    274                  curl_easy_setopt (eh, CURLOPT_TIMEOUT, 30L));
    275   res = curl_easy_perform (eh);
    276   if (CURLE_OK != res)
    277   {
    278     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    279                 "Failed to fetch `%s': %s\n",
    280                 url,
    281                 curl_easy_strerror (res));
    282     curl_easy_cleanup (eh);
    283     curl_slist_free_all (hdrs);
    284     GNUNET_free (url);
    285     return GNUNET_SYSERR;
    286   }
    287   GNUNET_assert (CURLE_OK ==
    288                  curl_easy_getinfo (eh, CURLINFO_RESPONSE_CODE, &code));
    289   curl_easy_cleanup (eh);
    290   curl_slist_free_all (hdrs);
    291   GNUNET_free (url);
    292   if (MHD_HTTP_OK != code)
    293   {
    294     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    295                 "Unexpected HTTP status %ld for Accept: %s\n",
    296                 code,
    297                 (NULL != accept) ? accept : "<none>");
    298     return GNUNET_SYSERR;
    299   }
    300   if (0 != strcmp (want_type,
    301                    h.content_type))
    302   {
    303     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    304                 "Accept: %s / Accept-Language: %s returned `%s', wanted `%s'\n",
    305                 (NULL != accept) ? accept : "<none>",
    306                 (NULL != accept_language) ? accept_language : "<none>",
    307                 h.content_type,
    308                 want_type);
    309     return GNUNET_SYSERR;
    310   }
    311   if (0 != strcmp (want_language,
    312                    h.content_language))
    313   {
    314     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    315                 "Accept: %s / Accept-Language: %s returned language `%s', wanted `%s'\n",
    316                 (NULL != accept) ? accept : "<none>",
    317                 (NULL != accept_language) ? accept_language : "<none>",
    318                 h.content_language,
    319                 want_language);
    320     return GNUNET_SYSERR;
    321   }
    322   return GNUNET_OK;
    323 }
    324 
    325 
    326 int
    327 main (int argc,
    328       const char *const argv[])
    329 {
    330   struct GNUNET_CONFIGURATION_Handle *cfg;
    331   struct MHD_Daemon *d;
    332   uint16_t port;
    333 
    334   (void) argc;
    335   (void) argv;
    336   GNUNET_log_setup ("test-legal",
    337                     "WARNING",
    338                     NULL);
    339   GNUNET_assert (CURLE_OK ==
    340                  curl_global_init (CURL_GLOBAL_DEFAULT));
    341   tmpdir = GNUNET_DISK_mkdtemp ("taler-test-legal");
    342   GNUNET_assert (NULL != tmpdir);
    343   /* Order of creation is deliberately "PDF last", as that is what
    344      exposed the format-preference bug. */
    345   GNUNET_assert (GNUNET_OK ==
    346                  write_terms ("en",
    347                               TERMS_VERSION ".txt",
    348                               "terms in plain text"));
    349   GNUNET_assert (GNUNET_OK ==
    350                  write_terms ("en",
    351                               TERMS_VERSION ".html",
    352                               "<html><body>terms</body></html>"));
    353   GNUNET_assert (GNUNET_OK ==
    354                  write_terms ("en",
    355                               TERMS_VERSION ".md",
    356                               "# terms"));
    357   GNUNET_assert (GNUNET_OK ==
    358                  write_terms ("en",
    359                               TERMS_VERSION ".pdf",
    360                               "%PDF-1.4 terms"));
    361   GNUNET_assert (GNUNET_OK ==
    362                  write_terms ("de",
    363                               TERMS_VERSION ".txt",
    364                               "AGB im Textformat"));
    365   GNUNET_assert (GNUNET_OK ==
    366                  write_terms ("de",
    367                               TERMS_VERSION ".pdf",
    368                               "%PDF-1.4 AGB"));
    369   cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ());
    370   GNUNET_CONFIGURATION_set_value_string (cfg,
    371                                          "test",
    372                                          "TERMS_DIR",
    373                                          tmpdir);
    374   GNUNET_CONFIGURATION_set_value_string (cfg,
    375                                          "test",
    376                                          "TERMS_ETAG",
    377                                          TERMS_VERSION);
    378   legal = TALER_MHD_legal_load (cfg,
    379                                 "test",
    380                                 "TERMS_DIR",
    381                                 "TERMS_ETAG");
    382   GNUNET_CONFIGURATION_destroy (cfg);
    383   GNUNET_assert (NULL != legal);
    384   d = MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD,
    385                         0 /* any free port */,
    386                         NULL, NULL,
    387                         &access_handler, NULL,
    388                         MHD_OPTION_END);
    389   GNUNET_assert (NULL != d);
    390   {
    391     const union MHD_DaemonInfo *di;
    392 
    393     di = MHD_get_daemon_info (d,
    394                               MHD_DAEMON_INFO_BIND_PORT);
    395     GNUNET_assert (NULL != di);
    396     port = di->port;
    397   }
    398 
    399   {
    400     struct
    401     {
    402       const char *accept;
    403       const char *accept_language;
    404       const char *want_type;
    405       const char *want_language;
    406     } tests[] = {
    407       /* A client without a format preference (curl, browsers, the
    408          wallet) must not be served the PDF. */
    409       { "*/*", NULL, "text/plain", "en" },
    410       /* No "Accept" header at all. */
    411       { NULL, NULL, "text/plain", "en" },
    412       /* A wildcard within "text" must not pick HTML over plain text. */
    413       { "text/*", NULL, "text/plain", "en" },
    414       /* An explicit preference still wins over our own ranking. */
    415       { "application/pdf", NULL, "application/pdf", "en" },
    416       { "text/html", NULL, "text/html", "en" },
    417       { "text/markdown", NULL, "text/markdown", "en" },
    418       { "text/plain;q=0.1, application/pdf;q=0.9", NULL,
    419         "application/pdf", "en" },
    420       /* Weights are the only way a client can convey a preference order,
    421          so they must decide both ways round -- including against our own
    422          ranking, which puts HTML above Markdown. */
    423       { "text/markdown, text/html;q=0.9", NULL, "text/markdown", "en" },
    424       { "text/html, text/markdown;q=0.9", NULL, "text/html", "en" },
    425       /* Format preference must not override the language preference. */
    426       { "*/*", "de", "text/plain", "de" },
    427       { "application/pdf", "de", "application/pdf", "de" },
    428       { NULL, NULL, NULL, NULL }
    429     };
    430 
    431     for (unsigned int i = 0; NULL != tests[i].want_type; i++)
    432       if (GNUNET_OK !=
    433           check_negotiation (port,
    434                              tests[i].accept,
    435                              tests[i].accept_language,
    436                              tests[i].want_type,
    437                              tests[i].want_language))
    438         global_ret = 1;
    439   }
    440   MHD_stop_daemon (d);
    441   TALER_MHD_legal_free (legal);
    442   GNUNET_assert (GNUNET_OK ==
    443                  GNUNET_DISK_directory_remove (tmpdir));
    444   GNUNET_free (tmpdir);
    445   curl_global_cleanup ();
    446   return global_ret;
    447 }
    448 
    449 
    450 /* end of test_legal.c */