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