Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2020 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 taler-merchant-httpd_templating.c
18 : * @brief logic to load and complete HTML templates
19 : * @author Christian Grothoff
20 : */
21 : #include "platform.h"
22 : #include <gnunet/gnunet_util_lib.h>
23 : #include <taler/taler_util.h>
24 : #include <taler/taler_mhd_lib.h>
25 : #include "taler-merchant-httpd_templating.h"
26 : #include "../mustach/mustach.h"
27 : #include "../mustach/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 const char *
76 0 : lookup_template (struct MHD_Connection *connection,
77 : const char *name)
78 : {
79 0 : struct TVE *best = NULL;
80 : const char *lang;
81 :
82 0 : lang = MHD_lookup_connection_value (connection,
83 : MHD_HEADER_KIND,
84 : MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
85 0 : if (NULL == lang)
86 0 : lang = "en";
87 : /* find best match by language */
88 0 : for (unsigned int i = 0; i<loaded_length; i++)
89 : {
90 0 : if (0 != strcmp (loaded[i].name,
91 : name))
92 0 : continue; /* does not match by name */
93 0 : if ( (NULL == best) ||
94 0 : (TALER_language_matches (lang,
95 0 : loaded[i].lang) >
96 0 : TALER_language_matches (lang,
97 0 : best->lang) ) )
98 0 : best = &loaded[i];
99 : }
100 0 : if (NULL == best)
101 : {
102 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
103 : "No templates found in `%s'\n",
104 : name);
105 0 : return NULL;
106 : }
107 0 : return best->value;
108 : }
109 :
110 : /**
111 : * Get the base URL for static resources.
112 : *
113 : * @param con the MHD connection
114 : * @param instance_id the instance ID
115 : * @returns the static files base URL, guaranteed
116 : * to have a trailing slash.
117 : */
118 : static char *
119 0 : make_static_url (struct MHD_Connection *con,
120 : const char *instance_id)
121 : {
122 : const char *host;
123 : const char *forwarded_host;
124 : const char *uri_path;
125 0 : struct GNUNET_Buffer buf = { 0 };
126 :
127 0 : host = MHD_lookup_connection_value (con,
128 : MHD_HEADER_KIND,
129 : "Host");
130 0 : forwarded_host = MHD_lookup_connection_value (con,
131 : MHD_HEADER_KIND,
132 : "X-Forwarded-Host");
133 :
134 0 : uri_path = MHD_lookup_connection_value (con,
135 : MHD_HEADER_KIND,
136 : "X-Forwarded-Prefix");
137 0 : if (NULL != forwarded_host)
138 0 : host = forwarded_host;
139 :
140 0 : if (NULL == host)
141 : {
142 0 : GNUNET_break (0);
143 0 : return NULL;
144 : }
145 :
146 0 : GNUNET_assert (NULL != instance_id);
147 :
148 0 : if (GNUNET_NO == TALER_mhd_is_https (con))
149 0 : GNUNET_buffer_write_str (&buf,
150 : "http://");
151 : else
152 0 : GNUNET_buffer_write_str (&buf,
153 : "https://");
154 0 : GNUNET_buffer_write_str (&buf,
155 : host);
156 0 : if (NULL != uri_path)
157 0 : GNUNET_buffer_write_path (&buf,
158 : uri_path);
159 0 : if (0 != strcmp ("default",
160 : instance_id))
161 : {
162 0 : GNUNET_buffer_write_path (&buf,
163 : "instances");
164 0 : GNUNET_buffer_write_path (&buf,
165 : instance_id);
166 : }
167 0 : GNUNET_buffer_write_path (&buf,
168 : "static/");
169 0 : return GNUNET_buffer_reap_str (&buf);
170 : }
171 :
172 :
173 :
174 :
175 : /**
176 : * Load a @a template and substitute using @a root, returning
177 : * the result to the @a connection with the given
178 : * @a http_status code.
179 : *
180 : * @param connection the connection we act upon
181 : * @param http_status code to use on success
182 : * @param template basename of the template to load
183 : * @param instance_id instance ID, used to compute static files URL
184 : * @param taler_uri value for "Taler:" header to set, or NULL
185 : * @param root JSON object to pass as the root context
186 : * @return #GNUNET_OK on success (reply queued), #GNUNET_NO if an error was queued,
187 : * #GNUNET_SYSERR on failure (to queue an error)
188 : */
189 : enum GNUNET_GenericReturnValue
190 0 : TMH_return_from_template (struct MHD_Connection *connection,
191 : unsigned int http_status,
192 : const char *template,
193 : const char *instance_id,
194 : const char *taler_uri,
195 : json_t *root)
196 : {
197 : struct MHD_Response *reply;
198 : char *body;
199 : size_t body_size;
200 :
201 : {
202 : const char *tmpl;
203 : int eno;
204 :
205 0 : tmpl = lookup_template (connection,
206 : template);
207 0 : if (NULL == tmpl)
208 : {
209 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
210 : "Failed to load template `%s'\n",
211 : template);
212 0 : if (MHD_YES !=
213 0 : TALER_MHD_reply_with_error (connection,
214 : MHD_HTTP_NOT_ACCEPTABLE,
215 : TALER_EC_MERCHANT_GENERIC_FAILED_TO_LOAD_TEMPLATE,
216 : template))
217 0 : return GNUNET_SYSERR;
218 0 : return GNUNET_NO;
219 : }
220 : /* Add default values to the context */
221 : {
222 0 : char *static_url = make_static_url (connection,
223 : instance_id);
224 0 : json_object_set (root,
225 : "static_url",
226 : json_string (static_url));
227 0 : GNUNET_free (static_url);
228 : }
229 0 : if (0 !=
230 0 : (eno = mustach_jansson (tmpl,
231 : root,
232 : &body,
233 : &body_size)))
234 : {
235 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
236 : "mustach failed on template `%s' with error %d\n",
237 : template,
238 : eno);
239 0 : if (MHD_YES !=
240 0 : TALER_MHD_reply_with_error (connection,
241 : MHD_HTTP_INTERNAL_SERVER_ERROR,
242 : TALER_EC_MERCHANT_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
243 : template))
244 0 : return GNUNET_SYSERR;
245 0 : return GNUNET_NO;
246 : }
247 : }
248 :
249 : /* try to compress reply if client allows it */
250 : {
251 0 : bool compressed = false;
252 :
253 0 : if (MHD_YES ==
254 0 : TALER_MHD_can_compress (connection))
255 : {
256 0 : compressed = TALER_MHD_body_compress ((void **) &body,
257 : &body_size);
258 : }
259 0 : reply = MHD_create_response_from_buffer (body_size,
260 : body,
261 : MHD_RESPMEM_MUST_FREE);
262 0 : if (NULL == reply)
263 : {
264 0 : GNUNET_break (0);
265 0 : return GNUNET_SYSERR;
266 : }
267 0 : if (compressed)
268 : {
269 0 : if (MHD_NO ==
270 0 : MHD_add_response_header (reply,
271 : MHD_HTTP_HEADER_CONTENT_ENCODING,
272 : "deflate"))
273 : {
274 0 : GNUNET_break (0);
275 0 : MHD_destroy_response (reply);
276 0 : return GNUNET_SYSERR;
277 : }
278 : }
279 : }
280 :
281 : /* Add standard headers */
282 0 : if (NULL != taler_uri)
283 0 : GNUNET_break (MHD_NO !=
284 : MHD_add_response_header (reply,
285 : "Taler",
286 : taler_uri));
287 0 : GNUNET_break (MHD_NO !=
288 : MHD_add_response_header (reply,
289 : MHD_HTTP_HEADER_CONTENT_TYPE,
290 : "text/html"));
291 :
292 : /* Actually return reply */
293 : {
294 : MHD_RESULT ret;
295 :
296 0 : ret = MHD_queue_response (connection,
297 : http_status,
298 : reply);
299 0 : MHD_destroy_response (reply);
300 0 : if (MHD_NO == ret)
301 0 : return GNUNET_SYSERR;
302 : }
303 0 : return GNUNET_OK;
304 : }
305 :
306 :
307 : /**
308 : * Function called with a template's filename.
309 : *
310 : * @param cls closure
311 : * @param filename complete filename (absolute path)
312 : * @return #GNUNET_OK to continue to iterate,
313 : * #GNUNET_NO to stop iteration with no error,
314 : * #GNUNET_SYSERR to abort iteration with error!
315 : */
316 : static int
317 15 : load_template (void *cls,
318 : const char *filename)
319 : {
320 : char *lang;
321 : char *end;
322 : int fd;
323 : struct stat sb;
324 : char *map;
325 : const char *name;
326 :
327 15 : if ('.' == filename[0])
328 0 : return GNUNET_OK;
329 :
330 15 : name = strrchr (filename,
331 : '/');
332 15 : if (NULL == name)
333 0 : name = filename;
334 : else
335 15 : name++;
336 15 : lang = strchr (name,
337 : '.');
338 15 : if (NULL == lang)
339 0 : return GNUNET_OK; /* name must include .$LANG */
340 15 : lang++;
341 15 : end = strchr (lang,
342 : '.');
343 15 : if ( (NULL == end) ||
344 15 : (0 != strcmp (end,
345 : ".must")) )
346 0 : return GNUNET_OK; /* name must end with '.must' */
347 :
348 : /* finally open template */
349 15 : fd = open (filename,
350 : O_RDONLY);
351 15 : if (-1 == fd)
352 : {
353 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
354 : "open",
355 : filename);
356 :
357 0 : return GNUNET_SYSERR;
358 : }
359 15 : if (0 !=
360 15 : fstat (fd,
361 : &sb))
362 : {
363 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
364 : "open",
365 : filename);
366 0 : GNUNET_break (0 == close (fd));
367 0 : return GNUNET_OK;
368 : }
369 15 : map = GNUNET_malloc_large (sb.st_size + 1);
370 15 : if (NULL == map)
371 : {
372 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
373 : "malloc");
374 0 : GNUNET_break (0 == close (fd));
375 0 : return GNUNET_SYSERR;
376 : }
377 15 : if (sb.st_size !=
378 15 : read (fd,
379 : map,
380 15 : sb.st_size))
381 : {
382 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
383 : "read",
384 : filename);
385 0 : GNUNET_break (0 == close (fd));
386 0 : return GNUNET_OK;
387 : }
388 15 : GNUNET_break (0 == close (fd));
389 15 : GNUNET_array_grow (loaded,
390 : loaded_length,
391 : loaded_length + 1);
392 15 : loaded[loaded_length - 1].name = GNUNET_strndup (name,
393 : (lang - 1) - name);
394 15 : loaded[loaded_length - 1].lang = GNUNET_strndup (lang,
395 : end - lang);
396 15 : loaded[loaded_length - 1].value = map;
397 15 : return GNUNET_OK;
398 : }
399 :
400 :
401 : /**
402 : * Preload templates.
403 : */
404 : int
405 3 : TMH_templating_init ()
406 : {
407 : char *dn;
408 : int ret;
409 :
410 : {
411 : char *path;
412 :
413 3 : path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR);
414 3 : if (NULL == path)
415 : {
416 0 : GNUNET_break (0);
417 0 : return GNUNET_SYSERR;
418 : }
419 3 : GNUNET_asprintf (&dn,
420 : "%s/merchant/templates/",
421 : path);
422 3 : GNUNET_free (path);
423 : }
424 3 : ret = GNUNET_DISK_directory_scan (dn,
425 : &load_template,
426 : NULL);
427 3 : GNUNET_free (dn);
428 3 : if (-1 == ret)
429 : {
430 0 : GNUNET_break (0);
431 0 : return GNUNET_SYSERR;
432 : }
433 3 : return GNUNET_OK;
434 : }
435 :
436 :
437 : /**
438 : * Nicely shut down.
439 : */
440 : void __attribute__ ((destructor))
441 15 : templating_fini ()
442 : {
443 30 : for (unsigned int i = 0; i<loaded_length; i++)
444 : {
445 15 : GNUNET_free (loaded[i].name);
446 15 : GNUNET_free (loaded[i].lang);
447 15 : GNUNET_free (loaded[i].value);
448 : }
449 15 : GNUNET_array_grow (loaded,
450 : loaded_length,
451 : 0);
452 15 : }
|