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