templating_api.c (15763B)
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 GNUNET_break (0 == 271 json_object_set_new ((json_t *) root, 272 "static_url", 273 json_string (static_url))); 274 GNUNET_free (static_url); 275 } 276 if (0 != 277 (eno = mustach_jansson_mem (tmpl, 278 0, 279 (json_t *) root, 280 Mustach_With_NoExtensions, 281 &body, 282 &body_size))) 283 { 284 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 285 "mustach failed on template `%s' with error %d\n", 286 template, 287 eno); 288 *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; 289 *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, 290 template); 291 return GNUNET_NO; 292 } 293 } 294 295 /* try to compress reply if client allows it */ 296 { 297 bool compressed = false; 298 299 if (TALER_MHD_CT_DEFLATE == 300 TALER_MHD_can_compress (connection, 301 TALER_MHD_CT_DEFLATE)) 302 { 303 compressed = TALER_MHD_body_compress ((void **) &body, 304 &body_size); 305 } 306 *reply = MHD_create_response_from_buffer (body_size, 307 body, 308 MHD_RESPMEM_MUST_FREE); 309 if (NULL == *reply) 310 { 311 GNUNET_break (0); 312 return GNUNET_SYSERR; 313 } 314 if (compressed) 315 { 316 if (MHD_NO == 317 MHD_add_response_header (*reply, 318 MHD_HTTP_HEADER_CONTENT_ENCODING, 319 "deflate")) 320 { 321 GNUNET_break (0); 322 MHD_destroy_response (*reply); 323 *reply = NULL; 324 return GNUNET_SYSERR; 325 } 326 } 327 } 328 if (NULL != tve->lang) 329 GNUNET_break (MHD_YES == 330 MHD_add_response_header (*reply, 331 MHD_HTTP_HEADER_CONTENT_LANGUAGE, 332 tve->lang)); 333 334 /* Add standard headers */ 335 if (NULL != taler_uri) 336 GNUNET_break (MHD_NO != 337 MHD_add_response_header (*reply, 338 "Taler", 339 taler_uri)); 340 return GNUNET_OK; 341 } 342 343 344 enum GNUNET_GenericReturnValue 345 TALER_TEMPLATING_reply (struct MHD_Connection *connection, 346 unsigned int http_status, 347 const char *template, 348 const char *instance_id, 349 const char *taler_uri, 350 const json_t *root) 351 { 352 enum GNUNET_GenericReturnValue res; 353 struct MHD_Response *reply; 354 enum MHD_Result ret; 355 356 res = TALER_TEMPLATING_build (connection, 357 &http_status, 358 template, 359 instance_id, 360 taler_uri, 361 root, 362 &reply); 363 if (GNUNET_SYSERR == res) 364 return res; 365 GNUNET_break (MHD_NO != 366 MHD_add_response_header (reply, 367 MHD_HTTP_HEADER_CONTENT_TYPE, 368 "text/html")); 369 // FIXME: set Vary header! 370 ret = MHD_queue_response (connection, 371 http_status, 372 reply); 373 MHD_destroy_response (reply); 374 if (MHD_NO == ret) 375 return GNUNET_SYSERR; 376 return (res == GNUNET_OK) 377 ? GNUNET_OK 378 : GNUNET_NO; 379 } 380 381 382 /** 383 * Function called with a template's filename. 384 * 385 * @param cls closure, NULL 386 * @param filename complete filename (absolute path) 387 * @return #GNUNET_OK to continue to iterate, 388 * #GNUNET_NO to stop iteration with no error, 389 * #GNUNET_SYSERR to abort iteration with error! 390 */ 391 static enum GNUNET_GenericReturnValue 392 load_template (void *cls, 393 const char *filename) 394 { 395 char *lang; 396 char *end; 397 int fd; 398 struct stat sb; 399 char *map; 400 const char *name; 401 402 (void) cls; 403 if ('.' == filename[0]) 404 return GNUNET_OK; 405 name = strrchr (filename, 406 '/'); 407 if (NULL == name) 408 name = filename; 409 else 410 name++; 411 lang = strchr (name, 412 '.'); 413 if (NULL == lang) 414 return GNUNET_OK; /* name must include .$LANG */ 415 lang++; 416 end = strchr (lang, 417 '.'); 418 if ( (NULL == end) || 419 (0 != strcmp (end, 420 ".must")) ) 421 return GNUNET_OK; /* name must end with '.must' */ 422 423 /* finally open template */ 424 fd = open (filename, 425 O_RDONLY); 426 if (-1 == fd) 427 { 428 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 429 "open", 430 filename); 431 432 return GNUNET_SYSERR; 433 } 434 if (0 != 435 fstat (fd, 436 &sb)) 437 { 438 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 439 "fstat", 440 filename); 441 GNUNET_break (0 == close (fd)); 442 return GNUNET_OK; 443 } 444 map = GNUNET_malloc_large (sb.st_size + 1); 445 if (NULL == map) 446 { 447 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 448 "malloc"); 449 GNUNET_break (0 == close (fd)); 450 return GNUNET_SYSERR; 451 } 452 if (sb.st_size != 453 read (fd, 454 map, 455 sb.st_size)) 456 { 457 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 458 "read", 459 filename); 460 GNUNET_break (0 == close (fd)); 461 return GNUNET_OK; 462 } 463 GNUNET_break (0 == close (fd)); 464 GNUNET_array_grow (loaded, 465 loaded_length, 466 loaded_length + 1); 467 loaded[loaded_length - 1].name = GNUNET_strndup (name, 468 (lang - 1) - name); 469 loaded[loaded_length - 1].lang = GNUNET_strndup (lang, 470 end - lang); 471 loaded[loaded_length - 1].value = map; 472 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 473 "Loading template `%s' (%s)\n", 474 filename, 475 loaded[loaded_length - 1].name); 476 return GNUNET_OK; 477 } 478 479 480 enum MHD_Result 481 TALER_TEMPLATING_reply_error ( 482 struct MHD_Connection *connection, 483 const char *template_basename, 484 unsigned int http_status, 485 enum TALER_ErrorCode ec, 486 const char *detail) 487 { 488 json_t *data; 489 enum GNUNET_GenericReturnValue ret; 490 491 data = GNUNET_JSON_PACK ( 492 GNUNET_JSON_pack_uint64 ("ec", 493 ec), 494 GNUNET_JSON_pack_string ("hint", 495 TALER_ErrorCode_get_hint (ec)), 496 GNUNET_JSON_pack_allow_null ( 497 GNUNET_JSON_pack_string ("detail", 498 detail)) 499 ); 500 ret = TALER_TEMPLATING_reply (connection, 501 http_status, 502 template_basename, 503 NULL, 504 NULL, 505 data); 506 json_decref (data); 507 switch (ret) 508 { 509 case GNUNET_OK: 510 return MHD_YES; 511 case GNUNET_NO: 512 return MHD_YES; 513 case GNUNET_SYSERR: 514 return MHD_NO; 515 } 516 GNUNET_assert (0); 517 return MHD_NO; 518 } 519 520 521 enum GNUNET_GenericReturnValue 522 TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd) 523 { 524 char *dn; 525 int ret; 526 527 { 528 char *path; 529 530 path = GNUNET_OS_installation_get_path (pd, 531 GNUNET_OS_IPK_DATADIR); 532 if (NULL == path) 533 { 534 GNUNET_break (0); 535 return GNUNET_SYSERR; 536 } 537 GNUNET_asprintf (&dn, 538 "%s/templates/", 539 path); 540 GNUNET_free (path); 541 } 542 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 543 "Loading templates from `%s'\n", 544 dn); 545 ret = GNUNET_DISK_directory_scan (dn, 546 &load_template, 547 NULL); 548 GNUNET_free (dn); 549 if (-1 == ret) 550 { 551 GNUNET_break (0); 552 return GNUNET_SYSERR; 553 } 554 return GNUNET_OK; 555 } 556 557 558 void 559 TALER_TEMPLATING_done (void) 560 { 561 for (unsigned int i = 0; i<loaded_length; i++) 562 { 563 GNUNET_free (loaded[i].name); 564 GNUNET_free (loaded[i].lang); 565 GNUNET_free (loaded[i].value); 566 } 567 GNUNET_array_grow (loaded, 568 loaded_length, 569 0); 570 } 571 572 573 /* end of templating_api.c */