templating_api.c (16055B)
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 double best_q = 0.0; 81 const char *lang; 82 83 lang = MHD_lookup_connection_value (connection, 84 MHD_HEADER_KIND, 85 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 86 if (NULL == lang) 87 lang = "en"; 88 /* find best match by language */ 89 for (unsigned int i = 0; i<loaded_length; i++) 90 { 91 double q; 92 93 if (0 != strcmp (loaded[i].name, 94 name)) 95 continue; /* does not match by name */ 96 if (NULL == loaded[i].lang) /* no language == always best match */ 97 return &loaded[i]; 98 q = TALER_pattern_matches (lang, 99 loaded[i].lang); 100 if (q < best_q) 101 continue; 102 best_q = q; 103 best = &loaded[i]; 104 } 105 if (NULL == best) 106 { 107 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 108 "No templates found for `%s'\n", 109 name); 110 return NULL; 111 } 112 return best; 113 } 114 115 116 /** 117 * Get the base URL for static resources. 118 * 119 * @param con the MHD connection 120 * @param instance_id the instance ID 121 * @returns the static files base URL, guaranteed 122 * to have a trailing slash. 123 */ 124 static char * 125 make_static_url (struct MHD_Connection *con, 126 const char *instance_id) 127 { 128 const char *host; 129 const char *forwarded_host; 130 const char *uri_path; 131 struct GNUNET_Buffer buf = { 0 }; 132 133 host = MHD_lookup_connection_value (con, 134 MHD_HEADER_KIND, 135 "Host"); 136 forwarded_host = MHD_lookup_connection_value (con, 137 MHD_HEADER_KIND, 138 "X-Forwarded-Host"); 139 140 uri_path = MHD_lookup_connection_value (con, 141 MHD_HEADER_KIND, 142 "X-Forwarded-Prefix"); 143 if (NULL != forwarded_host) 144 host = forwarded_host; 145 146 if (NULL == host) 147 { 148 GNUNET_break (0); 149 return NULL; 150 } 151 152 GNUNET_assert (NULL != instance_id); 153 154 if (GNUNET_NO == TALER_mhd_is_https (con)) 155 GNUNET_buffer_write_str (&buf, 156 "http://"); 157 else 158 GNUNET_buffer_write_str (&buf, 159 "https://"); 160 GNUNET_buffer_write_str (&buf, 161 host); 162 if (NULL != uri_path) 163 GNUNET_buffer_write_path (&buf, 164 uri_path); 165 if (0 != strcmp ("default", 166 instance_id)) 167 { 168 GNUNET_buffer_write_path (&buf, 169 "instances"); 170 GNUNET_buffer_write_path (&buf, 171 instance_id); 172 } 173 GNUNET_buffer_write_path (&buf, 174 "static/"); 175 return GNUNET_buffer_reap_str (&buf); 176 } 177 178 179 int 180 TALER_TEMPLATING_fill (const char *tmpl, 181 const json_t *root, 182 void **result, 183 size_t *result_size) 184 { 185 int eno; 186 187 if (0 != 188 (eno = mustach_jansson_mem (tmpl, 189 0, /* length of tmpl */ 190 (json_t *) root, 191 Mustach_With_AllExtensions, 192 (char **) result, 193 result_size))) 194 { 195 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 196 "mustach failed on template with error %d\n", 197 eno); 198 *result = NULL; 199 *result_size = 0; 200 return eno; 201 } 202 return eno; 203 } 204 205 206 int 207 TALER_TEMPLATING_fill2 (const void *tmpl, 208 size_t tmpl_len, 209 const json_t *root, 210 void **result, 211 size_t *result_size) 212 { 213 int eno; 214 215 if (0 != 216 (eno = mustach_jansson_mem (tmpl, 217 tmpl_len, 218 (json_t *) root, 219 Mustach_With_AllExtensions, 220 (char **) result, 221 result_size))) 222 { 223 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 224 "mustach failed on template with error %d\n", 225 eno); 226 *result = NULL; 227 *result_size = 0; 228 return eno; 229 } 230 return eno; 231 } 232 233 234 enum GNUNET_GenericReturnValue 235 TALER_TEMPLATING_build (struct MHD_Connection *connection, 236 unsigned int *http_status, 237 const char *template, 238 const char *instance_id, 239 const char *taler_uri, 240 const json_t *root, 241 struct MHD_Response **reply) 242 { 243 char *body; 244 size_t body_size; 245 struct TVE *tve; 246 247 { 248 const char *tmpl; 249 int eno; 250 251 tve = lookup_template (connection, 252 template); 253 if (NULL == tve) 254 { 255 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 256 "Failed to load template `%s'\n", 257 template); 258 *http_status = MHD_HTTP_NOT_ACCEPTABLE; 259 *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE, 260 template); 261 return GNUNET_NO; 262 } 263 tmpl = tve->value; 264 /* Add default values to the context */ 265 if (NULL != instance_id) 266 { 267 char *static_url = make_static_url (connection, 268 instance_id); 269 270 if (NULL != static_url) 271 { 272 GNUNET_break (0 == 273 json_object_set_new ((json_t *) root, 274 "static_url", 275 json_string (static_url))); 276 GNUNET_free (static_url); 277 } 278 } 279 if (0 != 280 (eno = mustach_jansson_mem (tmpl, 281 0, 282 (json_t *) root, 283 Mustach_With_NoExtensions, 284 &body, 285 &body_size))) 286 { 287 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 288 "mustach failed on template `%s' with error %d\n", 289 template, 290 eno); 291 *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; 292 *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, 293 template); 294 return GNUNET_NO; 295 } 296 } 297 298 /* try to compress reply if client allows it */ 299 { 300 bool compressed = false; 301 302 if (TALER_MHD_CT_DEFLATE == 303 TALER_MHD_can_compress (connection, 304 TALER_MHD_CT_DEFLATE)) 305 { 306 compressed = TALER_MHD_body_compress ((void **) &body, 307 &body_size); 308 } 309 *reply = MHD_create_response_from_buffer (body_size, 310 body, 311 MHD_RESPMEM_MUST_FREE); 312 if (NULL == *reply) 313 { 314 GNUNET_free (body); 315 GNUNET_break (0); 316 return GNUNET_SYSERR; 317 } 318 if (compressed) 319 { 320 if (MHD_NO == 321 MHD_add_response_header (*reply, 322 MHD_HTTP_HEADER_CONTENT_ENCODING, 323 "deflate")) 324 { 325 GNUNET_break (0); 326 MHD_destroy_response (*reply); 327 *reply = NULL; 328 return GNUNET_SYSERR; 329 } 330 } 331 } 332 if (NULL != tve->lang) 333 GNUNET_break (MHD_YES == 334 MHD_add_response_header (*reply, 335 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 336 tve->lang)); 337 338 /* Add standard headers */ 339 if (NULL != taler_uri) 340 GNUNET_break (MHD_NO != 341 MHD_add_response_header (*reply, 342 "Taler", 343 taler_uri)); 344 return GNUNET_OK; 345 } 346 347 348 enum GNUNET_GenericReturnValue 349 TALER_TEMPLATING_reply (struct MHD_Connection *connection, 350 unsigned int http_status, 351 const char *template, 352 const char *instance_id, 353 const char *taler_uri, 354 const json_t *root) 355 { 356 enum GNUNET_GenericReturnValue res; 357 struct MHD_Response *reply; 358 enum MHD_Result ret; 359 360 res = TALER_TEMPLATING_build (connection, 361 &http_status, 362 template, 363 instance_id, 364 taler_uri, 365 root, 366 &reply); 367 if (GNUNET_SYSERR == res) 368 return res; 369 GNUNET_break (MHD_NO != 370 MHD_add_response_header (reply, 371 MHD_HTTP_HEADER_CONTENT_TYPE, 372 "text/html")); 373 /* FIXME: also vary by compression? */ 374 GNUNET_break (MHD_NO != 375 MHD_add_response_header ( 376 reply, 377 MHD_HTTP_HEADER_VARY, 378 MHD_HTTP_HEADER_ACCEPT_LANGUAGE)); 379 ret = MHD_queue_response (connection, 380 http_status, 381 reply); 382 MHD_destroy_response (reply); 383 if (MHD_NO == ret) 384 return GNUNET_SYSERR; 385 return (res == GNUNET_OK) 386 ? GNUNET_OK 387 : GNUNET_NO; 388 } 389 390 391 /** 392 * Function called with a template's filename. 393 * 394 * @param cls closure, NULL 395 * @param filename complete filename (absolute path) 396 * @return #GNUNET_OK to continue to iterate, 397 * #GNUNET_NO to stop iteration with no error, 398 * #GNUNET_SYSERR to abort iteration with error! 399 */ 400 static enum GNUNET_GenericReturnValue 401 load_template (void *cls, 402 const char *filename) 403 { 404 const char *lang; 405 const char *end; 406 int fd; 407 struct stat sb; 408 char *map; 409 const char *name; 410 411 (void) cls; 412 if ('.' == filename[0]) 413 return GNUNET_OK; 414 name = strrchr (filename, 415 '/'); 416 if (NULL == name) 417 name = filename; 418 else 419 name++; 420 lang = strchr (name, 421 '.'); 422 if (NULL == lang) 423 return GNUNET_OK; /* name must include .$LANG */ 424 lang++; 425 end = strchr (lang, 426 '.'); 427 if ( (NULL == end) || 428 (0 != strcmp (end, 429 ".must")) ) 430 return GNUNET_OK; /* name must end with '.must' */ 431 432 /* finally open template */ 433 fd = open (filename, 434 O_RDONLY); 435 if (-1 == fd) 436 { 437 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 438 "open", 439 filename); 440 441 return GNUNET_SYSERR; 442 } 443 if (0 != 444 fstat (fd, 445 &sb)) 446 { 447 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 448 "fstat", 449 filename); 450 GNUNET_break (0 == close (fd)); 451 return GNUNET_OK; 452 } 453 map = GNUNET_malloc_large (sb.st_size + 1); 454 if (NULL == map) 455 { 456 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 457 "malloc"); 458 GNUNET_break (0 == close (fd)); 459 return GNUNET_SYSERR; 460 } 461 if (sb.st_size != 462 read (fd, 463 map, 464 sb.st_size)) 465 { 466 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 467 "read", 468 filename); 469 GNUNET_break (0 == close (fd)); 470 return GNUNET_OK; 471 } 472 GNUNET_break (0 == close (fd)); 473 GNUNET_array_grow (loaded, 474 loaded_length, 475 loaded_length + 1); 476 loaded[loaded_length - 1].name = GNUNET_strndup (name, 477 (lang - 1) - name); 478 loaded[loaded_length - 1].lang = GNUNET_strndup (lang, 479 end - lang); 480 loaded[loaded_length - 1].value = map; 481 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 482 "Loading template `%s' (%s)\n", 483 filename, 484 loaded[loaded_length - 1].name); 485 return GNUNET_OK; 486 } 487 488 489 enum MHD_Result 490 TALER_TEMPLATING_reply_error ( 491 struct MHD_Connection *connection, 492 const char *template_basename, 493 unsigned int http_status, 494 enum TALER_ErrorCode ec, 495 const char *detail) 496 { 497 json_t *data; 498 enum GNUNET_GenericReturnValue ret; 499 500 data = GNUNET_JSON_PACK ( 501 GNUNET_JSON_pack_uint64 ("ec", 502 ec), 503 GNUNET_JSON_pack_string ("hint", 504 TALER_ErrorCode_get_hint (ec)), 505 GNUNET_JSON_pack_allow_null ( 506 GNUNET_JSON_pack_string ("detail", 507 detail)) 508 ); 509 ret = TALER_TEMPLATING_reply (connection, 510 http_status, 511 template_basename, 512 NULL, 513 NULL, 514 data); 515 json_decref (data); 516 switch (ret) 517 { 518 case GNUNET_OK: 519 return MHD_YES; 520 case GNUNET_NO: 521 return MHD_YES; 522 case GNUNET_SYSERR: 523 return MHD_NO; 524 } 525 GNUNET_assert (0); 526 return MHD_NO; 527 } 528 529 530 enum GNUNET_GenericReturnValue 531 TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd) 532 { 533 char *dn; 534 int ret; 535 536 { 537 char *path; 538 539 path = GNUNET_OS_installation_get_path (pd, 540 GNUNET_OS_IPK_DATADIR); 541 if (NULL == path) 542 { 543 GNUNET_break (0); 544 return GNUNET_SYSERR; 545 } 546 GNUNET_asprintf (&dn, 547 "%s/templates/", 548 path); 549 GNUNET_free (path); 550 } 551 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 552 "Loading templates from `%s'\n", 553 dn); 554 ret = GNUNET_DISK_directory_scan (dn, 555 &load_template, 556 NULL); 557 GNUNET_free (dn); 558 if (-1 == ret) 559 { 560 GNUNET_break (0); 561 return GNUNET_SYSERR; 562 } 563 return GNUNET_OK; 564 } 565 566 567 void 568 TALER_TEMPLATING_done (void) 569 { 570 for (unsigned int i = 0; i<loaded_length; i++) 571 { 572 GNUNET_free (loaded[i].name); 573 GNUNET_free (loaded[i].lang); 574 GNUNET_free (loaded[i].value); 575 } 576 GNUNET_array_grow (loaded, 577 loaded_length, 578 0); 579 } 580 581 582 /* end of templating_api.c */