templating_api.c (16493B)
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 lookup_template (struct MHD_Connection *connection, 77 const char *name) 78 { 79 struct TVE *best = NULL; 80 struct TVE *fallback = NULL; 81 double best_q = 0.0; 82 const char *lang; 83 84 lang = MHD_lookup_connection_value (connection, 85 MHD_HEADER_KIND, 86 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 87 if (NULL == lang) 88 lang = "en"; 89 /* find best match by language */ 90 for (unsigned int i = 0; i<loaded_length; i++) 91 { 92 double q; 93 94 if (0 != strcmp (loaded[i].name, 95 name)) 96 continue; /* does not match by name */ 97 if (NULL == loaded[i].lang) /* no language == always best match */ 98 return &loaded[i]; 99 if (NULL == fallback) 100 fallback = &loaded[i]; /* in case nothing matches the language */ 101 q = TALER_pattern_matches (lang, 102 loaded[i].lang); 103 if (q <= best_q) 104 continue; /* not a strictly better match than what we have */ 105 best_q = q; 106 best = &loaded[i]; 107 } 108 if (NULL == best) 109 best = fallback; /* no variant matched the language, serve any */ 110 if (NULL == best) 111 { 112 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 113 "No templates found for `%s'\n", 114 name); 115 return NULL; 116 } 117 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 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 struct GNUNET_Buffer buf = { 0 }; 137 138 host = MHD_lookup_connection_value (con, 139 MHD_HEADER_KIND, 140 "Host"); 141 forwarded_host = MHD_lookup_connection_value (con, 142 MHD_HEADER_KIND, 143 "X-Forwarded-Host"); 144 145 uri_path = MHD_lookup_connection_value (con, 146 MHD_HEADER_KIND, 147 "X-Forwarded-Prefix"); 148 if (NULL != forwarded_host) 149 host = forwarded_host; 150 151 if (NULL == host) 152 { 153 GNUNET_break (0); 154 return NULL; 155 } 156 157 GNUNET_assert (NULL != instance_id); 158 159 if (GNUNET_NO == TALER_mhd_is_https (con)) 160 GNUNET_buffer_write_str (&buf, 161 "http://"); 162 else 163 GNUNET_buffer_write_str (&buf, 164 "https://"); 165 GNUNET_buffer_write_str (&buf, 166 host); 167 if (NULL != uri_path) 168 GNUNET_buffer_write_path (&buf, 169 uri_path); 170 if (0 != strcmp ("default", 171 instance_id)) 172 { 173 GNUNET_buffer_write_path (&buf, 174 "instances"); 175 GNUNET_buffer_write_path (&buf, 176 instance_id); 177 } 178 GNUNET_buffer_write_path (&buf, 179 "static/"); 180 return GNUNET_buffer_reap_str (&buf); 181 } 182 183 184 int 185 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 if (0 != 193 (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 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 201 "mustach failed on template with error %d\n", 202 eno); 203 *result = NULL; 204 *result_size = 0; 205 return eno; 206 } 207 return eno; 208 } 209 210 211 int 212 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 if (0 != 221 (eno = mustach_jansson_mem (tmpl, 222 tmpl_len, 223 (json_t *) root, 224 Mustach_With_AllExtensions, 225 (char **) result, 226 result_size))) 227 { 228 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 229 "mustach failed on template with error %d\n", 230 eno); 231 *result = NULL; 232 *result_size = 0; 233 return eno; 234 } 235 return eno; 236 } 237 238 239 enum GNUNET_GenericReturnValue 240 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 tve = lookup_template (connection, 257 template); 258 if (NULL == tve) 259 { 260 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 261 "Failed to load template `%s'\n", 262 template); 263 *http_status = MHD_HTTP_NOT_ACCEPTABLE; 264 *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE, 265 template); 266 return GNUNET_NO; 267 } 268 tmpl = tve->value; 269 /* Add default values to the context */ 270 if (NULL != instance_id) 271 { 272 char *static_url = make_static_url (connection, 273 instance_id); 274 275 if (NULL != static_url) 276 { 277 GNUNET_break (0 == 278 json_object_set_new ((json_t *) root, 279 "static_url", 280 json_string (static_url))); 281 GNUNET_free (static_url); 282 } 283 } 284 if (0 != 285 (eno = mustach_jansson_mem (tmpl, 286 0, 287 (json_t *) root, 288 Mustach_With_NoExtensions, 289 &body, 290 &body_size))) 291 { 292 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 293 "mustach failed on template `%s' with error %d\n", 294 template, 295 eno); 296 *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; 297 *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, 298 template); 299 return GNUNET_NO; 300 } 301 } 302 303 /* try to compress reply if client allows it */ 304 { 305 bool compressed = false; 306 307 if (TALER_MHD_CT_DEFLATE == 308 TALER_MHD_can_compress (connection, 309 TALER_MHD_CT_DEFLATE)) 310 { 311 compressed = TALER_MHD_body_compress ((void **) &body, 312 &body_size); 313 } 314 *reply = MHD_create_response_from_buffer (body_size, 315 body, 316 MHD_RESPMEM_MUST_FREE); 317 if (NULL == *reply) 318 { 319 GNUNET_free (body); 320 GNUNET_break (0); 321 return GNUNET_SYSERR; 322 } 323 if (compressed) 324 { 325 if (MHD_NO == 326 MHD_add_response_header (*reply, 327 MHD_HTTP_HEADER_CONTENT_ENCODING, 328 "deflate")) 329 { 330 GNUNET_break (0); 331 MHD_destroy_response (*reply); 332 *reply = NULL; 333 return GNUNET_SYSERR; 334 } 335 } 336 } 337 if (NULL != tve->lang) 338 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 if (NULL != taler_uri) 345 GNUNET_break (MHD_NO != 346 MHD_add_response_header (*reply, 347 "Taler", 348 taler_uri)); 349 return GNUNET_OK; 350 } 351 352 353 enum GNUNET_GenericReturnValue 354 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 res = TALER_TEMPLATING_build (connection, 366 &http_status, 367 template, 368 instance_id, 369 taler_uri, 370 root, 371 &reply); 372 if (GNUNET_SYSERR == res) 373 return res; 374 if (GNUNET_OK == res) 375 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 GNUNET_break (MHD_NO != 383 MHD_add_response_header ( 384 reply, 385 MHD_HTTP_HEADER_VARY, 386 MHD_HTTP_HEADER_ACCEPT_LANGUAGE)); 387 ret = MHD_queue_response (connection, 388 http_status, 389 reply); 390 MHD_destroy_response (reply); 391 if (MHD_NO == ret) 392 return GNUNET_SYSERR; 393 return (res == GNUNET_OK) 394 ? GNUNET_OK 395 : 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 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 if ('.' == filename[0]) 421 return GNUNET_OK; 422 name = strrchr (filename, 423 '/'); 424 if (NULL == name) 425 name = filename; 426 else 427 name++; 428 lang = strchr (name, 429 '.'); 430 if (NULL == lang) 431 return GNUNET_OK; /* name must include .$LANG */ 432 lang++; 433 end = strchr (lang, 434 '.'); 435 if ( (NULL == end) || 436 (0 != strcmp (end, 437 ".must")) ) 438 return GNUNET_OK; /* name must end with '.must' */ 439 440 /* finally open template */ 441 fd = open (filename, 442 O_RDONLY); 443 if (-1 == fd) 444 { 445 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 446 "open", 447 filename); 448 449 return GNUNET_SYSERR; 450 } 451 if (0 != 452 fstat (fd, 453 &sb)) 454 { 455 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 456 "fstat", 457 filename); 458 GNUNET_break (0 == close (fd)); 459 return GNUNET_OK; 460 } 461 map = GNUNET_malloc_large (sb.st_size + 1); 462 if (NULL == map) 463 { 464 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 465 "malloc"); 466 GNUNET_break (0 == close (fd)); 467 return GNUNET_SYSERR; 468 } 469 if (sb.st_size != 470 read (fd, 471 map, 472 sb.st_size)) 473 { 474 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 475 "read", 476 filename); 477 GNUNET_free (map); 478 GNUNET_break (0 == close (fd)); 479 return GNUNET_OK; 480 } 481 GNUNET_break (0 == close (fd)); 482 GNUNET_array_grow (loaded, 483 loaded_length, 484 loaded_length + 1); 485 loaded[loaded_length - 1].name = GNUNET_strndup (name, 486 (lang - 1) - name); 487 loaded[loaded_length - 1].lang = GNUNET_strndup (lang, 488 end - lang); 489 loaded[loaded_length - 1].value = map; 490 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 491 "Loading template `%s' (%s)\n", 492 filename, 493 loaded[loaded_length - 1].name); 494 return GNUNET_OK; 495 } 496 497 498 enum MHD_Result 499 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 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 ret = TALER_TEMPLATING_reply (connection, 519 http_status, 520 template_basename, 521 NULL, 522 NULL, 523 data); 524 json_decref (data); 525 switch (ret) 526 { 527 case GNUNET_OK: 528 return MHD_YES; 529 case GNUNET_NO: 530 return MHD_YES; 531 case GNUNET_SYSERR: 532 return MHD_NO; 533 } 534 GNUNET_assert (0); 535 return MHD_NO; 536 } 537 538 539 enum GNUNET_GenericReturnValue 540 TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd) 541 { 542 char *dn; 543 int ret; 544 545 { 546 char *path; 547 548 path = GNUNET_OS_installation_get_path (pd, 549 GNUNET_OS_IPK_DATADIR); 550 if (NULL == path) 551 { 552 GNUNET_break (0); 553 return GNUNET_SYSERR; 554 } 555 GNUNET_asprintf (&dn, 556 "%s/templates/", 557 path); 558 GNUNET_free (path); 559 } 560 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 561 "Loading templates from `%s'\n", 562 dn); 563 ret = GNUNET_DISK_directory_scan (dn, 564 &load_template, 565 NULL); 566 GNUNET_free (dn); 567 if (-1 == ret) 568 { 569 GNUNET_break (0); 570 return GNUNET_SYSERR; 571 } 572 return GNUNET_OK; 573 } 574 575 576 void 577 TALER_TEMPLATING_done (void) 578 { 579 for (unsigned int i = 0; i<loaded_length; i++) 580 { 581 GNUNET_free (loaded[i].name); 582 GNUNET_free (loaded[i].lang); 583 GNUNET_free (loaded[i].value); 584 } 585 GNUNET_array_grow (loaded, 586 loaded_length, 587 0); 588 } 589 590 591 /* end of templating_api.c */