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 = ▮ 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 */