templating_api.c (15498B)
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 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 return GNUNET_OK; 334 } 335 336 337 enum GNUNET_GenericReturnValue 338 TALER_TEMPLATING_reply (struct MHD_Connection *connection, 339 unsigned int http_status, 340 const char *template, 341 const char *instance_id, 342 const char *taler_uri, 343 const json_t *root) 344 { 345 enum GNUNET_GenericReturnValue res; 346 struct MHD_Response *reply; 347 enum MHD_Result ret; 348 349 res = TALER_TEMPLATING_build (connection, 350 &http_status, 351 template, 352 instance_id, 353 taler_uri, 354 root, 355 &reply); 356 if (GNUNET_SYSERR == res) 357 return res; 358 GNUNET_break (MHD_NO != 359 MHD_add_response_header (reply, 360 MHD_HTTP_HEADER_CONTENT_TYPE, 361 "text/html")); 362 // FIXME: set Vary header! 363 ret = MHD_queue_response (connection, 364 http_status, 365 reply); 366 MHD_destroy_response (reply); 367 if (MHD_NO == ret) 368 return GNUNET_SYSERR; 369 return (res == GNUNET_OK) 370 ? GNUNET_OK 371 : GNUNET_NO; 372 } 373 374 375 /** 376 * Function called with a template's filename. 377 * 378 * @param cls closure, NULL 379 * @param filename complete filename (absolute path) 380 * @return #GNUNET_OK to continue to iterate, 381 * #GNUNET_NO to stop iteration with no error, 382 * #GNUNET_SYSERR to abort iteration with error! 383 */ 384 static enum GNUNET_GenericReturnValue 385 load_template (void *cls, 386 const char *filename) 387 { 388 char *lang; 389 char *end; 390 int fd; 391 struct stat sb; 392 char *map; 393 const char *name; 394 395 (void) cls; 396 if ('.' == filename[0]) 397 return GNUNET_OK; 398 name = strrchr (filename, 399 '/'); 400 if (NULL == name) 401 name = filename; 402 else 403 name++; 404 lang = strchr (name, 405 '.'); 406 if (NULL == lang) 407 return GNUNET_OK; /* name must include .$LANG */ 408 lang++; 409 end = strchr (lang, 410 '.'); 411 if ( (NULL == end) || 412 (0 != strcmp (end, 413 ".must")) ) 414 return GNUNET_OK; /* name must end with '.must' */ 415 416 /* finally open template */ 417 fd = open (filename, 418 O_RDONLY); 419 if (-1 == fd) 420 { 421 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 422 "open", 423 filename); 424 425 return GNUNET_SYSERR; 426 } 427 if (0 != 428 fstat (fd, 429 &sb)) 430 { 431 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 432 "fstat", 433 filename); 434 GNUNET_break (0 == close (fd)); 435 return GNUNET_OK; 436 } 437 map = GNUNET_malloc_large (sb.st_size + 1); 438 if (NULL == map) 439 { 440 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 441 "malloc"); 442 GNUNET_break (0 == close (fd)); 443 return GNUNET_SYSERR; 444 } 445 if (sb.st_size != 446 read (fd, 447 map, 448 sb.st_size)) 449 { 450 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 451 "read", 452 filename); 453 GNUNET_break (0 == close (fd)); 454 return GNUNET_OK; 455 } 456 GNUNET_break (0 == close (fd)); 457 GNUNET_array_grow (loaded, 458 loaded_length, 459 loaded_length + 1); 460 loaded[loaded_length - 1].name = GNUNET_strndup (name, 461 (lang - 1) - name); 462 loaded[loaded_length - 1].lang = GNUNET_strndup (lang, 463 end - lang); 464 loaded[loaded_length - 1].value = map; 465 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 466 "Loading template `%s' (%s)\n", 467 filename, 468 loaded[loaded_length - 1].name); 469 return GNUNET_OK; 470 } 471 472 473 enum MHD_Result 474 TALER_TEMPLATING_reply_error ( 475 struct MHD_Connection *connection, 476 const char *template_basename, 477 unsigned int http_status, 478 enum TALER_ErrorCode ec, 479 const char *detail) 480 { 481 json_t *data; 482 enum GNUNET_GenericReturnValue ret; 483 484 data = GNUNET_JSON_PACK ( 485 GNUNET_JSON_pack_uint64 ("ec", 486 ec), 487 GNUNET_JSON_pack_string ("hint", 488 TALER_ErrorCode_get_hint (ec)), 489 GNUNET_JSON_pack_allow_null ( 490 GNUNET_JSON_pack_string ("detail", 491 detail)) 492 ); 493 ret = TALER_TEMPLATING_reply (connection, 494 http_status, 495 template_basename, 496 NULL, 497 NULL, 498 data); 499 json_decref (data); 500 switch (ret) 501 { 502 case GNUNET_OK: 503 return MHD_YES; 504 case GNUNET_NO: 505 return MHD_YES; 506 case GNUNET_SYSERR: 507 return MHD_NO; 508 } 509 GNUNET_assert (0); 510 return MHD_NO; 511 } 512 513 514 enum GNUNET_GenericReturnValue 515 TALER_TEMPLATING_init (const struct GNUNET_OS_ProjectData *pd) 516 { 517 char *dn; 518 int ret; 519 520 { 521 char *path; 522 523 path = GNUNET_OS_installation_get_path (pd, 524 GNUNET_OS_IPK_DATADIR); 525 if (NULL == path) 526 { 527 GNUNET_break (0); 528 return GNUNET_SYSERR; 529 } 530 GNUNET_asprintf (&dn, 531 "%s/templates/", 532 path); 533 GNUNET_free (path); 534 } 535 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 536 "Loading templates from `%s'\n", 537 dn); 538 ret = GNUNET_DISK_directory_scan (dn, 539 &load_template, 540 NULL); 541 GNUNET_free (dn); 542 if (-1 == ret) 543 { 544 GNUNET_break (0); 545 return GNUNET_SYSERR; 546 } 547 return GNUNET_OK; 548 } 549 550 551 void 552 TALER_TEMPLATING_done (void) 553 { 554 for (unsigned int i = 0; i<loaded_length; i++) 555 { 556 GNUNET_free (loaded[i].name); 557 GNUNET_free (loaded[i].lang); 558 GNUNET_free (loaded[i].value); 559 } 560 GNUNET_array_grow (loaded, 561 loaded_length, 562 0); 563 } 564 565 566 /* end of templating_api.c */