Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2020, 2022 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU 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 General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file templating_api.c
18 : * @brief logic to load and complete HTML templates
19 : * @author Christian Grothoff
20 : */
21 : #include "platform.h" /* UNNECESSARY? */
22 : #include <gnunet/gnunet_util_lib.h>
23 : #include "taler/taler_util.h"
24 : #include "taler/taler_mhd_lib.h"
25 : #include "taler/taler_templating_lib.h"
26 : #include "mustach.h"
27 : #include "mustach-jansson.h"
28 : #include <gnunet/gnunet_mhd_compat.h>
29 :
30 :
31 : /**
32 : * Entry in a key-value array we use to cache templates.
33 : */
34 : struct TVE
35 : {
36 : /**
37 : * A name, used as the key. NULL for the last entry.
38 : */
39 : char *name;
40 :
41 : /**
42 : * Language the template is in.
43 : */
44 : char *lang;
45 :
46 : /**
47 : * 0-terminated (!) file data to return for @e name and @e lang.
48 : */
49 : char *value;
50 :
51 : };
52 :
53 :
54 : /**
55 : * Array of templates loaded into RAM.
56 : */
57 : static struct TVE *loaded;
58 :
59 : /**
60 : * Length of the #loaded array.
61 : */
62 : static unsigned int loaded_length;
63 :
64 :
65 : /**
66 : * Load Mustach template into memory. Note that we intentionally cache
67 : * failures, that is if we ever failed to load a template, we will never try
68 : * again.
69 : *
70 : * @param connection the connection we act upon
71 : * @param name name of the template file to load
72 : * (MUST be a 'static' string in memory!)
73 : * @return NULL on error, otherwise the template
74 : */
75 : static struct TVE *
76 3 : lookup_template (struct MHD_Connection *connection,
77 : const char *name)
78 : {
79 3 : struct TVE *best = NULL;
80 3 : struct TVE *fallback = NULL;
81 3 : double best_q = 0.0;
82 : const char *lang;
83 :
84 3 : lang = MHD_lookup_connection_value (connection,
85 : MHD_HEADER_KIND,
86 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
87 3 : if (NULL == lang)
88 3 : lang = "en";
89 : /* find best match by language */
90 63 : for (unsigned int i = 0; i<loaded_length; i++)
91 : {
92 : double q;
93 :
94 60 : if (0 != strcmp (loaded[i].name,
95 : name))
96 57 : continue; /* does not match by name */
97 3 : if (NULL == loaded[i].lang) /* no language == always best match */
98 0 : return &loaded[i];
99 3 : if (NULL == fallback)
100 3 : fallback = &loaded[i]; /* in case nothing matches the language */
101 3 : q = TALER_pattern_matches (lang,
102 3 : loaded[i].lang);
103 3 : if (q <= best_q)
104 0 : continue; /* not a strictly better match than what we have */
105 3 : best_q = q;
106 3 : best = &loaded[i];
107 : }
108 3 : if (NULL == best)
109 0 : best = fallback; /* no variant matched the language, serve any */
110 3 : if (NULL == best)
111 : {
112 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
113 : "No templates found for `%s'\n",
114 : name);
115 0 : return NULL;
116 : }
117 3 : return best;
118 : }
119 :
120 :
121 : /**
122 : * Get the base URL for static resources.
123 : *
124 : * @param con the MHD connection
125 : * @param instance_id the instance ID
126 : * @returns the static files base URL, guaranteed
127 : * to have a trailing slash.
128 : */
129 : static char *
130 0 : make_static_url (struct MHD_Connection *con,
131 : const char *instance_id)
132 : {
133 : const char *host;
134 : const char *forwarded_host;
135 : const char *uri_path;
136 0 : struct GNUNET_Buffer buf = { 0 };
137 :
138 0 : host = MHD_lookup_connection_value (con,
139 : MHD_HEADER_KIND,
140 : "Host");
141 0 : forwarded_host = MHD_lookup_connection_value (con,
142 : MHD_HEADER_KIND,
143 : "X-Forwarded-Host");
144 :
145 0 : uri_path = MHD_lookup_connection_value (con,
146 : MHD_HEADER_KIND,
147 : "X-Forwarded-Prefix");
148 0 : if (NULL != forwarded_host)
149 0 : host = forwarded_host;
150 :
151 0 : if (NULL == host)
152 : {
153 0 : GNUNET_break (0);
154 0 : return NULL;
155 : }
156 :
157 0 : GNUNET_assert (NULL != instance_id);
158 :
159 0 : if (GNUNET_NO == TALER_mhd_is_https (con))
160 0 : GNUNET_buffer_write_str (&buf,
161 : "http://");
162 : else
163 0 : GNUNET_buffer_write_str (&buf,
164 : "https://");
165 0 : GNUNET_buffer_write_str (&buf,
166 : host);
167 0 : if (NULL != uri_path)
168 0 : GNUNET_buffer_write_path (&buf,
169 : uri_path);
170 0 : if (0 != strcmp ("default",
171 : instance_id))
172 : {
173 0 : GNUNET_buffer_write_path (&buf,
174 : "instances");
175 0 : GNUNET_buffer_write_path (&buf,
176 : instance_id);
177 : }
178 0 : GNUNET_buffer_write_path (&buf,
179 : "static/");
180 0 : return GNUNET_buffer_reap_str (&buf);
181 : }
182 :
183 :
184 : int
185 0 : TALER_TEMPLATING_fill (const char *tmpl,
186 : const json_t *root,
187 : void **result,
188 : size_t *result_size)
189 : {
190 : int eno;
191 :
192 0 : if (0 !=
193 0 : (eno = mustach_jansson_mem (tmpl,
194 : 0, /* length of tmpl */
195 : (json_t *) root,
196 : Mustach_With_AllExtensions,
197 : (char **) result,
198 : result_size)))
199 : {
200 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
201 : "mustach failed on template with error %d\n",
202 : eno);
203 0 : *result = NULL;
204 0 : *result_size = 0;
205 0 : return eno;
206 : }
207 0 : return eno;
208 : }
209 :
210 :
211 : int
212 0 : TALER_TEMPLATING_fill2 (const void *tmpl,
213 : size_t tmpl_len,
214 : const json_t *root,
215 : void **result,
216 : size_t *result_size)
217 : {
218 : int eno;
219 :
220 0 : if (0 !=
221 0 : (eno = mustach_jansson_mem (tmpl,
222 : tmpl_len,
223 : (json_t *) root,
224 : Mustach_With_AllExtensions,
225 : (char **) result,
226 : result_size)))
227 : {
228 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
229 : "mustach failed on template with error %d\n",
230 : eno);
231 0 : *result = NULL;
232 0 : *result_size = 0;
233 0 : return eno;
234 : }
235 0 : return eno;
236 : }
237 :
238 :
239 : enum GNUNET_GenericReturnValue
240 3 : TALER_TEMPLATING_build (struct MHD_Connection *connection,
241 : unsigned int *http_status,
242 : const char *template,
243 : const char *instance_id,
244 : const char *taler_uri,
245 : const json_t *root,
246 : struct MHD_Response **reply)
247 : {
248 : char *body;
249 : size_t body_size;
250 : struct TVE *tve;
251 :
252 : {
253 : const char *tmpl;
254 : int eno;
255 :
256 3 : tve = lookup_template (connection,
257 : template);
258 3 : if (NULL == tve)
259 : {
260 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
261 : "Failed to load template `%s'\n",
262 : template);
263 0 : *http_status = MHD_HTTP_NOT_ACCEPTABLE;
264 0 : *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE,
265 : template);
266 0 : return GNUNET_NO;
267 : }
268 3 : tmpl = tve->value;
269 : /* Add default values to the context */
270 3 : if (NULL != instance_id)
271 : {
272 0 : char *static_url = make_static_url (connection,
273 : instance_id);
274 :
275 0 : if (NULL != static_url)
276 : {
277 0 : GNUNET_break (0 ==
278 : json_object_set_new ((json_t *) root,
279 : "static_url",
280 : json_string (static_url)));
281 0 : GNUNET_free (static_url);
282 : }
283 : }
284 3 : if (0 !=
285 3 : (eno = mustach_jansson_mem (tmpl,
286 : 0,
287 : (json_t *) root,
288 : Mustach_With_NoExtensions,
289 : &body,
290 : &body_size)))
291 : {
292 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
293 : "mustach failed on template `%s' with error %d\n",
294 : template,
295 : eno);
296 0 : *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
297 0 : *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
298 : template);
299 0 : return GNUNET_NO;
300 : }
301 : }
302 :
303 : /* try to compress reply if client allows it */
304 : {
305 3 : bool compressed = false;
306 :
307 3 : if (TALER_MHD_CT_DEFLATE ==
308 3 : TALER_MHD_can_compress (connection,
309 : TALER_MHD_CT_DEFLATE))
310 : {
311 3 : compressed = TALER_MHD_body_compress ((void **) &body,
312 : &body_size);
313 : }
314 3 : *reply = MHD_create_response_from_buffer (body_size,
315 : body,
316 : MHD_RESPMEM_MUST_FREE);
317 3 : if (NULL == *reply)
318 : {
319 0 : GNUNET_free (body);
320 0 : GNUNET_break (0);
321 0 : return GNUNET_SYSERR;
322 : }
323 3 : if (compressed)
324 : {
325 3 : if (MHD_NO ==
326 3 : MHD_add_response_header (*reply,
327 : MHD_HTTP_HEADER_CONTENT_ENCODING,
328 : "deflate"))
329 : {
330 0 : GNUNET_break (0);
331 0 : MHD_destroy_response (*reply);
332 0 : *reply = NULL;
333 0 : return GNUNET_SYSERR;
334 : }
335 : }
336 : }
337 3 : if (NULL != tve->lang)
338 3 : GNUNET_break (MHD_YES ==
339 : MHD_add_response_header (*reply,
340 : MHD_HTTP_HEADER_CONTENT_LANGUAGE,
341 : tve->lang));
342 :
343 : /* Add standard headers */
344 3 : if (NULL != taler_uri)
345 0 : GNUNET_break (MHD_NO !=
346 : MHD_add_response_header (*reply,
347 : "Taler",
348 : taler_uri));
349 3 : return GNUNET_OK;
350 : }
351 :
352 :
353 : enum GNUNET_GenericReturnValue
354 0 : TALER_TEMPLATING_reply (struct MHD_Connection *connection,
355 : unsigned int http_status,
356 : const char *template,
357 : const char *instance_id,
358 : const char *taler_uri,
359 : const json_t *root)
360 : {
361 : enum GNUNET_GenericReturnValue res;
362 : struct MHD_Response *reply;
363 : enum MHD_Result ret;
364 :
365 0 : res = TALER_TEMPLATING_build (connection,
366 : &http_status,
367 : template,
368 : instance_id,
369 : taler_uri,
370 : root,
371 : &reply);
372 0 : if (GNUNET_SYSERR == res)
373 0 : return res;
374 0 : if (GNUNET_OK == res)
375 0 : GNUNET_break (MHD_NO !=
376 : MHD_add_response_header (reply,
377 : MHD_HTTP_HEADER_CONTENT_TYPE,
378 : "text/html"));
379 : /* Note: on failure, @a reply is a JSON error message
380 : which already comes with its own content type. */
381 : /* FIXME: also vary by compression? */
382 0 : GNUNET_break (MHD_NO !=
383 : MHD_add_response_header (
384 : reply,
385 : MHD_HTTP_HEADER_VARY,
386 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE));
387 0 : ret = MHD_queue_response (connection,
388 : http_status,
389 : reply);
390 0 : MHD_destroy_response (reply);
391 0 : if (MHD_NO == ret)
392 0 : return GNUNET_SYSERR;
393 : return (res == GNUNET_OK)
394 : ? GNUNET_OK
395 0 : : GNUNET_NO;
396 : }
397 :
398 :
399 : /**
400 : * Function called with a template's filename.
401 : *
402 : * @param cls closure, NULL
403 : * @param filename complete filename (absolute path)
404 : * @return #GNUNET_OK to continue to iterate,
405 : * #GNUNET_NO to stop iteration with no error,
406 : * #GNUNET_SYSERR to abort iteration with error!
407 : */
408 : static enum GNUNET_GenericReturnValue
409 360 : load_template (void *cls,
410 : const char *filename)
411 : {
412 : const char *lang;
413 : const char *end;
414 : int fd;
415 : struct stat sb;
416 : char *map;
417 : const char *name;
418 :
419 : (void) cls;
420 360 : if ('.' == filename[0])
421 0 : return GNUNET_OK;
422 360 : name = strrchr (filename,
423 : '/');
424 360 : if (NULL == name)
425 0 : name = filename;
426 : else
427 360 : name++;
428 360 : lang = strchr (name,
429 : '.');
430 360 : if (NULL == lang)
431 0 : return GNUNET_OK; /* name must include .$LANG */
432 360 : lang++;
433 360 : end = strchr (lang,
434 : '.');
435 360 : if ( (NULL == end) ||
436 360 : (0 != strcmp (end,
437 : ".must")) )
438 0 : return GNUNET_OK; /* name must end with '.must' */
439 :
440 : /* finally open template */
441 360 : fd = open (filename,
442 : O_RDONLY);
443 360 : if (-1 == fd)
444 : {
445 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
446 : "open",
447 : filename);
448 :
449 0 : return GNUNET_SYSERR;
450 : }
451 360 : if (0 !=
452 360 : fstat (fd,
453 : &sb))
454 : {
455 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
456 : "fstat",
457 : filename);
458 0 : GNUNET_break (0 == close (fd));
459 0 : return GNUNET_OK;
460 : }
461 360 : map = GNUNET_malloc_large (sb.st_size + 1);
462 360 : if (NULL == map)
463 : {
464 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
465 : "malloc");
466 0 : GNUNET_break (0 == close (fd));
467 0 : return GNUNET_SYSERR;
468 : }
469 360 : if (sb.st_size !=
470 360 : read (fd,
471 : map,
472 360 : sb.st_size))
473 : {
474 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
475 : "read",
476 : filename);
477 0 : GNUNET_free (map);
478 0 : GNUNET_break (0 == close (fd));
479 0 : return GNUNET_OK;
480 : }
481 360 : GNUNET_break (0 == close (fd));
482 360 : GNUNET_array_grow (loaded,
483 : loaded_length,
484 : loaded_length + 1);
485 360 : loaded[loaded_length - 1].name = GNUNET_strndup (name,
486 : (lang - 1) - name);
487 360 : loaded[loaded_length - 1].lang = GNUNET_strndup (lang,
488 : end - lang);
489 360 : loaded[loaded_length - 1].value = map;
490 360 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
491 : "Loading template `%s' (%s)\n",
492 : filename,
493 : loaded[loaded_length - 1].name);
494 360 : return GNUNET_OK;
495 : }
496 :
497 :
498 : enum MHD_Result
499 0 : TALER_TEMPLATING_reply_error (
500 : struct MHD_Connection *connection,
501 : const char *template_basename,
502 : unsigned int http_status,
503 : enum TALER_ErrorCode ec,
504 : const char *detail)
505 : {
506 : json_t *data;
507 : enum GNUNET_GenericReturnValue ret;
508 :
509 0 : data = GNUNET_JSON_PACK (
510 : GNUNET_JSON_pack_uint64 ("ec",
511 : ec),
512 : GNUNET_JSON_pack_string ("hint",
513 : TALER_ErrorCode_get_hint (ec)),
514 : GNUNET_JSON_pack_allow_null (
515 : GNUNET_JSON_pack_string ("detail",
516 : detail))
517 : );
518 0 : ret = TALER_TEMPLATING_reply (connection,
519 : http_status,
520 : template_basename,
521 : NULL,
522 : NULL,
523 : data);
524 0 : json_decref (data);
525 0 : switch (ret)
526 : {
527 0 : case GNUNET_OK:
528 0 : return MHD_YES;
529 0 : case GNUNET_NO:
530 0 : return MHD_YES;
531 0 : case GNUNET_SYSERR:
532 0 : return MHD_NO;
533 : }
534 0 : GNUNET_assert (0);
535 : return MHD_NO;
536 : }
537 :
538 :
539 : enum GNUNET_GenericReturnValue
540 18 : TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd)
541 : {
542 : char *dn;
543 : int ret;
544 :
545 : {
546 : char *path;
547 :
548 18 : path = GNUNET_OS_installation_get_path (pd,
549 : GNUNET_OS_IPK_DATADIR);
550 18 : if (NULL == path)
551 : {
552 0 : GNUNET_break (0);
553 0 : return GNUNET_SYSERR;
554 : }
555 18 : GNUNET_asprintf (&dn,
556 : "%s/templates/",
557 : path);
558 18 : GNUNET_free (path);
559 : }
560 18 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
561 : "Loading templates from `%s'\n",
562 : dn);
563 18 : ret = GNUNET_DISK_directory_scan (dn,
564 : &load_template,
565 : NULL);
566 18 : GNUNET_free (dn);
567 18 : if (-1 == ret)
568 : {
569 0 : GNUNET_break (0);
570 0 : return GNUNET_SYSERR;
571 : }
572 18 : return GNUNET_OK;
573 : }
574 :
575 :
576 : void
577 18 : TALER_TEMPLATING_done (void)
578 : {
579 378 : for (unsigned int i = 0; i<loaded_length; i++)
580 : {
581 360 : GNUNET_free (loaded[i].name);
582 360 : GNUNET_free (loaded[i].lang);
583 360 : GNUNET_free (loaded[i].value);
584 : }
585 18 : GNUNET_array_grow (loaded,
586 : loaded_length,
587 : 0);
588 18 : }
589 :
590 :
591 : /* end of templating_api.c */
|