LCOV - code coverage report
Current view: top level - mhd - test_legal.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 89.9 % 119 107
Test Date: 2026-09-09 15:11:34 Functions: 100.0 % 7 7

            Line data    Source code
       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          308 : extract_header (const char *buf,
      72              :                 size_t len,
      73              :                 const char *name,
      74              :                 char *dst,
      75              :                 size_t dst_size)
      76              : {
      77          308 :   size_t nlen = strlen (name);
      78              :   size_t vlen;
      79              : 
      80          308 :   if ( (len < nlen + 1) ||
      81          286 :        (0 != strncasecmp (buf,
      82              :                           name,
      83           22 :                           nlen)) ||
      84           22 :        (':' != buf[nlen]) )
      85          286 :     return;
      86           22 :   buf += nlen + 1;
      87           22 :   len -= nlen + 1;
      88           44 :   while ( (0 != len) &&
      89           44 :           (isspace ((int) *buf)) )
      90              :   {
      91           22 :     buf++;
      92           22 :     len--;
      93              :   }
      94           66 :   while ( (0 != len) &&
      95           66 :           (isspace ((int) buf[len - 1])) )
      96           44 :     len--;
      97           22 :   vlen = GNUNET_MIN (len,
      98              :                      dst_size - 1);
      99           22 :   memcpy (dst,
     100              :           buf,
     101              :           vlen);
     102           22 :   dst[vlen] = '\0';
     103              : }
     104              : 
     105              : 
     106              : /**
     107              :  * Function called by CURL for each response header.
     108              :  */
     109              : static size_t
     110          154 : header_cb (char *buffer,
     111              :            size_t size,
     112              :            size_t nitems,
     113              :            void *userdata)
     114              : {
     115          154 :   struct Headers *h = userdata;
     116          154 :   size_t len = size * nitems;
     117              : 
     118          154 :   extract_header (buffer,
     119              :                   len,
     120              :                   "Content-Type",
     121          154 :                   h->content_type,
     122              :                   sizeof (h->content_type));
     123          154 :   extract_header (buffer,
     124              :                   len,
     125              :                   "Content-Language",
     126          154 :                   h->content_language,
     127              :                   sizeof (h->content_language));
     128          154 :   return len;
     129              : }
     130              : 
     131              : 
     132              : /**
     133              :  * Function called by CURL with the response body; we discard it.
     134              :  */
     135              : static size_t
     136           11 : body_cb (char *buffer,
     137              :          size_t size,
     138              :          size_t nitems,
     139              :          void *userdata)
     140              : {
     141              :   (void) buffer;
     142              :   (void) userdata;
     143           11 :   return size * nitems;
     144              : }
     145              : 
     146              : 
     147              : /**
     148              :  * Handle a request by returning our terms of service.
     149              :  */
     150              : static enum MHD_Result
     151           22 : 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           22 :   if (NULL == *con_cls)
     169              :   {
     170           11 :     *con_cls = &marker;
     171           11 :     return MHD_YES;
     172              :   }
     173           11 :   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            6 : write_terms (const char *lang,
     183              :              const char *name,
     184              :              const char *content)
     185              : {
     186              :   char *dn;
     187              :   char *fn;
     188            6 :   enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
     189              : 
     190            6 :   GNUNET_asprintf (&dn,
     191              :                    "%s/%s",
     192              :                    tmpdir,
     193              :                    lang);
     194            6 :   GNUNET_asprintf (&fn,
     195              :                    "%s/%s",
     196              :                    dn,
     197              :                    name);
     198            6 :   if (GNUNET_OK ==
     199            6 :       GNUNET_DISK_directory_create (dn))
     200            6 :     ret = (GNUNET_SYSERR ==
     201            6 :            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            6 :           : GNUNET_OK;
     208            6 :   GNUNET_free (fn);
     209            6 :   GNUNET_free (dn);
     210            6 :   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           11 : 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           11 :   struct curl_slist *hdrs = NULL;
     235           11 :   struct Headers h = { 0 };
     236              :   char *url;
     237              :   char *hdr;
     238              :   long code;
     239              : 
     240           11 :   GNUNET_asprintf (&url,
     241              :                    "http://localhost:%u/terms",
     242              :                    (unsigned int) port);
     243           11 :   eh = curl_easy_init ();
     244           11 :   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           11 :   GNUNET_asprintf (&hdr,
     248              :                    "Accept:%s%s",
     249              :                    (NULL != accept) ? " " : "",
     250              :                    (NULL != accept) ? accept : "");
     251           11 :   hdrs = curl_slist_append (hdrs,
     252              :                             hdr);
     253           11 :   GNUNET_free (hdr);
     254           11 :   if (NULL != accept_language)
     255              :   {
     256            2 :     GNUNET_asprintf (&hdr,
     257              :                      "Accept-Language: %s",
     258              :                      accept_language);
     259            2 :     hdrs = curl_slist_append (hdrs,
     260              :                               hdr);
     261            2 :     GNUNET_free (hdr);
     262              :   }
     263           11 :   GNUNET_assert (CURLE_OK ==
     264              :                  curl_easy_setopt (eh, CURLOPT_URL, url));
     265           11 :   GNUNET_assert (CURLE_OK ==
     266              :                  curl_easy_setopt (eh, CURLOPT_HTTPHEADER, hdrs));
     267           11 :   GNUNET_assert (CURLE_OK ==
     268              :                  curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &header_cb));
     269           11 :   GNUNET_assert (CURLE_OK ==
     270              :                  curl_easy_setopt (eh, CURLOPT_HEADERDATA, &h));
     271           11 :   GNUNET_assert (CURLE_OK ==
     272              :                  curl_easy_setopt (eh, CURLOPT_WRITEFUNCTION, &body_cb));
     273           11 :   GNUNET_assert (CURLE_OK ==
     274              :                  curl_easy_setopt (eh, CURLOPT_TIMEOUT, 30L));
     275           11 :   res = curl_easy_perform (eh);
     276           11 :   if (CURLE_OK != res)
     277              :   {
     278            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     279              :                 "Failed to fetch `%s': %s\n",
     280              :                 url,
     281              :                 curl_easy_strerror (res));
     282            0 :     curl_easy_cleanup (eh);
     283            0 :     curl_slist_free_all (hdrs);
     284            0 :     GNUNET_free (url);
     285            0 :     return GNUNET_SYSERR;
     286              :   }
     287           11 :   GNUNET_assert (CURLE_OK ==
     288              :                  curl_easy_getinfo (eh, CURLINFO_RESPONSE_CODE, &code));
     289           11 :   curl_easy_cleanup (eh);
     290           11 :   curl_slist_free_all (hdrs);
     291           11 :   GNUNET_free (url);
     292           11 :   if (MHD_HTTP_OK != code)
     293              :   {
     294            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     295              :                 "Unexpected HTTP status %ld for Accept: %s\n",
     296              :                 code,
     297              :                 (NULL != accept) ? accept : "<none>");
     298            0 :     return GNUNET_SYSERR;
     299              :   }
     300           11 :   if (0 != strcmp (want_type,
     301              :                    h.content_type))
     302              :   {
     303            0 :     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            0 :     return GNUNET_SYSERR;
     310              :   }
     311           11 :   if (0 != strcmp (want_language,
     312              :                    h.content_language))
     313              :   {
     314            0 :     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            0 :     return GNUNET_SYSERR;
     321              :   }
     322           11 :   return GNUNET_OK;
     323              : }
     324              : 
     325              : 
     326              : int
     327            1 : 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            1 :   GNUNET_log_setup ("test-legal",
     337              :                     "WARNING",
     338              :                     NULL);
     339            1 :   GNUNET_assert (CURLE_OK ==
     340              :                  curl_global_init (CURL_GLOBAL_DEFAULT));
     341            1 :   tmpdir = GNUNET_DISK_mkdtemp ("taler-test-legal");
     342            1 :   GNUNET_assert (NULL != tmpdir);
     343              :   /* Order of creation is deliberately "PDF last", as that is what
     344              :      exposed the format-preference bug. */
     345            1 :   GNUNET_assert (GNUNET_OK ==
     346              :                  write_terms ("en",
     347              :                               TERMS_VERSION ".txt",
     348              :                               "terms in plain text"));
     349            1 :   GNUNET_assert (GNUNET_OK ==
     350              :                  write_terms ("en",
     351              :                               TERMS_VERSION ".html",
     352              :                               "<html><body>terms</body></html>"));
     353            1 :   GNUNET_assert (GNUNET_OK ==
     354              :                  write_terms ("en",
     355              :                               TERMS_VERSION ".md",
     356              :                               "# terms"));
     357            1 :   GNUNET_assert (GNUNET_OK ==
     358              :                  write_terms ("en",
     359              :                               TERMS_VERSION ".pdf",
     360              :                               "%PDF-1.4 terms"));
     361            1 :   GNUNET_assert (GNUNET_OK ==
     362              :                  write_terms ("de",
     363              :                               TERMS_VERSION ".txt",
     364              :                               "AGB im Textformat"));
     365            1 :   GNUNET_assert (GNUNET_OK ==
     366              :                  write_terms ("de",
     367              :                               TERMS_VERSION ".pdf",
     368              :                               "%PDF-1.4 AGB"));
     369            1 :   cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ());
     370            1 :   GNUNET_CONFIGURATION_set_value_string (cfg,
     371              :                                          "test",
     372              :                                          "TERMS_DIR",
     373              :                                          tmpdir);
     374            1 :   GNUNET_CONFIGURATION_set_value_string (cfg,
     375              :                                          "test",
     376              :                                          "TERMS_ETAG",
     377              :                                          TERMS_VERSION);
     378            1 :   legal = TALER_MHD_legal_load (cfg,
     379              :                                 "test",
     380              :                                 "TERMS_DIR",
     381              :                                 "TERMS_ETAG");
     382            1 :   GNUNET_CONFIGURATION_destroy (cfg);
     383            1 :   GNUNET_assert (NULL != legal);
     384            1 :   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            1 :   GNUNET_assert (NULL != d);
     390              :   {
     391              :     const union MHD_DaemonInfo *di;
     392              : 
     393            1 :     di = MHD_get_daemon_info (d,
     394              :                               MHD_DAEMON_INFO_BIND_PORT);
     395            1 :     GNUNET_assert (NULL != di);
     396            1 :     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            1 :     } 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           12 :     for (unsigned int i = 0; NULL != tests[i].want_type; i++)
     432           11 :       if (GNUNET_OK !=
     433           11 :           check_negotiation (port,
     434              :                              tests[i].accept,
     435              :                              tests[i].accept_language,
     436              :                              tests[i].want_type,
     437              :                              tests[i].want_language))
     438            0 :         global_ret = 1;
     439              :   }
     440            1 :   MHD_stop_daemon (d);
     441            1 :   TALER_MHD_legal_free (legal);
     442            1 :   GNUNET_assert (GNUNET_OK ==
     443              :                  GNUNET_DISK_directory_remove (tmpdir));
     444            1 :   GNUNET_free (tmpdir);
     445            1 :   curl_global_cleanup ();
     446            1 :   return global_ret;
     447              : }
     448              : 
     449              : 
     450              : /* end of test_legal.c */
        

Generated by: LCOV version 2.0-1