mhd_typst.c (20312B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025 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 mhd_typst.c 18 * @brief MHD utility functions for PDF generation 19 * @author Christian Grothoff 20 * 21 * 22 */ 23 #include "taler/platform.h" 24 #include "taler/taler_util.h" 25 #include "taler/taler_mhd_lib.h" 26 #include <microhttpd.h> 27 28 29 /** 30 * Information about a specific typst invocation. 31 */ 32 struct TypstStage 33 { 34 /** 35 * Name of the FIFO for the typst output. 36 */ 37 char *filename; 38 39 /** 40 * Typst context we are part of. 41 */ 42 struct TALER_MHD_TypstContext *tc; 43 44 /** 45 * Handle to the typst process. 46 */ 47 struct GNUNET_OS_Process *proc; 48 49 /** 50 * Handle to be notified about stage completion. 51 */ 52 struct GNUNET_ChildWaitHandle *cwh; 53 54 }; 55 56 57 struct TALER_MHD_TypstContext 58 { 59 60 /** 61 * Directory where we create temporary files (or FIFOs) for the IPC. 62 */ 63 char *tmpdir; 64 65 /** 66 * Array of stages producing PDFs to be combined. 67 */ 68 struct TypstStage *stages; 69 70 /** 71 * Handle for pdftk combining the various PDFs. 72 */ 73 struct GNUNET_OS_Process *proc; 74 75 /** 76 * Handle to wait for @e proc to complete. 77 */ 78 struct GNUNET_ChildWaitHandle *cwh; 79 80 /** 81 * Callback to call on the final result. 82 */ 83 TALER_MHD_TypstResultCallback cb; 84 85 /** 86 * Closure for @e cb 87 */ 88 void *cb_cls; 89 90 /** 91 * Task for async work. 92 */ 93 struct GNUNET_SCHEDULER_Task *t; 94 95 /** 96 * Name of the final file created by pdftk. 97 */ 98 char *output_file; 99 100 /** 101 * Length of the @e stages array. 102 */ 103 unsigned int num_stages; 104 105 /** 106 * Number of still active stages. 107 */ 108 unsigned int active_stages; 109 110 /** 111 * Should the directory be removed when done? 112 */ 113 bool remove_on_exit; 114 }; 115 116 117 void 118 TALER_MHD_typst_cancel (struct TALER_MHD_TypstContext *tc) 119 { 120 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 121 "Cleaning up TypstContext\n"); 122 if (NULL != tc->t) 123 { 124 GNUNET_SCHEDULER_cancel (tc->t); 125 tc->t = NULL; 126 } 127 for (unsigned int i = 0; i<tc->num_stages; i++) 128 { 129 struct TypstStage *stage = &tc->stages[i]; 130 131 if (NULL != stage->cwh) 132 { 133 GNUNET_wait_child_cancel (stage->cwh); 134 stage->cwh = NULL; 135 } 136 if (NULL != stage->proc) 137 { 138 GNUNET_break (0 == 139 GNUNET_OS_process_kill (stage->proc, 140 SIGKILL)); 141 GNUNET_OS_process_destroy (stage->proc); 142 stage->proc = NULL; 143 } 144 GNUNET_free (stage->filename); 145 } 146 GNUNET_free (tc->stages); 147 if (NULL != tc->cwh) 148 { 149 GNUNET_wait_child_cancel (tc->cwh); 150 tc->cwh = NULL; 151 } 152 if (NULL != tc->proc) 153 { 154 GNUNET_break (0 == 155 GNUNET_OS_process_kill (tc->proc, 156 SIGKILL)); 157 GNUNET_OS_process_destroy (tc->proc); 158 } 159 GNUNET_free (tc->output_file); 160 if (NULL != tc->tmpdir) 161 { 162 if (tc->remove_on_exit) 163 GNUNET_DISK_directory_remove (tc->tmpdir); 164 GNUNET_free (tc->tmpdir); 165 } 166 GNUNET_free (tc); 167 } 168 169 170 /** 171 * Create file in @a tmpdir with one of the PDF inputs. 172 * 173 * @param[out] stage initialized stage data 174 * @param tmpdir where to place temporary files 175 * @param data input JSON with PDF data 176 * @return true on success 177 */ 178 static bool 179 inline_pdf_stage (struct TypstStage *stage, 180 const char *tmpdir, 181 const json_t *data) 182 { 183 const char *str = json_string_value (data); 184 char *fn; 185 size_t n; 186 void *b; 187 int fd; 188 189 if (NULL == str) 190 { 191 GNUNET_break (0); 192 return false; 193 } 194 b = NULL; 195 n = GNUNET_STRINGS_base64_decode (str, 196 strlen (str), 197 &b); 198 if (NULL == b) 199 { 200 GNUNET_break (0); 201 return false; 202 } 203 GNUNET_asprintf (&fn, 204 "%s/external-", 205 tmpdir); 206 stage->filename = GNUNET_DISK_mktemp (fn); 207 if (NULL == stage->filename) 208 { 209 GNUNET_break (0); 210 GNUNET_free (b); 211 GNUNET_free (fn); 212 return false; 213 } 214 GNUNET_free (fn); 215 fd = open (stage->filename, 216 O_WRONLY | O_TRUNC, 217 S_IRUSR | S_IWUSR); 218 if (-1 == fd) 219 { 220 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 221 "open", 222 stage->filename); 223 GNUNET_free (b); 224 GNUNET_free (stage->filename); 225 return false; 226 } 227 228 { 229 size_t off = 0; 230 231 while (off < n) 232 { 233 ssize_t r; 234 235 r = write (fd, 236 b + off, 237 n - off); 238 if (-1 == r) 239 { 240 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 241 "write", 242 stage->filename); 243 GNUNET_break (0 == close (fd)); 244 GNUNET_free (b); 245 GNUNET_free (stage->filename); 246 return false; 247 } 248 off += r; 249 } 250 } 251 GNUNET_break (0 == close (fd)); 252 return true; 253 } 254 255 256 /** 257 * Generate a response for @a tc indicating an error of type @a ec. 258 * 259 * @param[in,out] tc context to fail 260 * @param ec error code to return 261 * @param hint hint text to return 262 */ 263 static void 264 typst_context_fail (struct TALER_MHD_TypstContext *tc, 265 enum TALER_ErrorCode ec, 266 const char *hint) 267 { 268 struct TALER_MHD_TypstResponse resp = { 269 .ec = ec, 270 .details.hint = hint 271 }; 272 273 if (NULL != tc->cb) 274 { 275 tc->cb (tc->cb_cls, 276 &resp); 277 tc->cb = NULL; 278 } 279 } 280 281 282 /** 283 * Called when the pdftk helper exited. 284 * 285 * @param cls our `struct TALER_MHD_TypstContext *` 286 * @param type type of the process 287 * @param exit_code status code of the process 288 */ 289 static void 290 pdftk_done_cb (void *cls, 291 enum GNUNET_OS_ProcessStatusType type, 292 long unsigned int exit_code) 293 { 294 struct TALER_MHD_TypstContext *tc = cls; 295 296 tc->cwh = NULL; 297 GNUNET_OS_process_destroy (tc->proc); 298 tc->proc = NULL; 299 switch (type) 300 { 301 case GNUNET_OS_PROCESS_UNKNOWN: 302 GNUNET_assert (0); 303 return; 304 case GNUNET_OS_PROCESS_RUNNING: 305 /* we should not get this notification */ 306 GNUNET_break (0); 307 return; 308 case GNUNET_OS_PROCESS_STOPPED: 309 /* Someone is SIGSTOPing our helper!? */ 310 GNUNET_break (0); 311 return; 312 case GNUNET_OS_PROCESS_EXITED: 313 if (0 != exit_code) 314 { 315 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 316 "pdftk exited with status %d\n", 317 (int) exit_code); 318 typst_context_fail (tc, 319 TALER_EC_EXCHANGE_GENERIC_PDFTK_FAILURE, 320 "pdftk failed"); 321 } 322 else 323 { 324 struct TALER_MHD_TypstResponse resp = { 325 .ec = TALER_EC_NONE, 326 .details.filename = tc->output_file, 327 }; 328 329 GNUNET_assert (NULL != tc->cb); 330 tc->cb (tc->cb_cls, 331 &resp); 332 tc->cb = NULL; 333 } 334 break; 335 case GNUNET_OS_PROCESS_SIGNALED: 336 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 337 "pdftk died with signal %d\n", 338 (int) exit_code); 339 typst_context_fail (tc, 340 TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH, 341 "pdftk killed by signal"); 342 break; 343 } 344 TALER_MHD_typst_cancel (tc); 345 } 346 347 348 /** 349 * Function called once all of the individual stages are done. 350 * Triggers the pdftk run for @a tc. 351 * 352 * @param[in,out] cls a `struct TALER_MHD_TypstContext *` context to run pdftk for 353 */ 354 static void 355 complete_response (void *cls) 356 { 357 struct TALER_MHD_TypstContext *tc = cls; 358 const char *argv[tc->num_stages + 5]; 359 360 tc->t = NULL; 361 argv[0] = "pdftk"; 362 for (unsigned int i = 0; i<tc->num_stages; i++) 363 argv[i + 1] = tc->stages[i].filename; 364 argv[tc->num_stages + 1] = "cat"; 365 argv[tc->num_stages + 2] = "output"; 366 argv[tc->num_stages + 3] = tc->output_file; 367 argv[tc->num_stages + 4] = NULL; 368 tc->proc = GNUNET_OS_start_process_vap ( 369 GNUNET_OS_INHERIT_STD_ERR, 370 NULL, 371 NULL, 372 NULL, 373 "pdftk", 374 (char **) argv); 375 if (NULL == tc->proc) 376 { 377 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 378 "fork"); 379 TALER_MHD_typst_cancel (tc); 380 return; 381 } 382 tc->cwh = GNUNET_wait_child (tc->proc, 383 &pdftk_done_cb, 384 tc); 385 GNUNET_assert (NULL != tc->cwh); 386 } 387 388 389 /** 390 * Cancel typst. Wrapper task to do so asynchronously. 391 * 392 * @param[in] cls a `struct TALER_MHD_TypstContext` 393 */ 394 static void 395 cancel_async (void *cls) 396 { 397 struct TALER_MHD_TypstContext *tc = cls; 398 399 tc->t = NULL; 400 TALER_MHD_typst_cancel (tc); 401 } 402 403 404 /** 405 * Called when a typst helper exited. 406 * 407 * @param cls our `struct TypstStage *` 408 * @param type type of the process 409 * @param exit_code status code of the process 410 */ 411 static void 412 typst_done_cb (void *cls, 413 enum GNUNET_OS_ProcessStatusType type, 414 long unsigned int exit_code) 415 { 416 struct TypstStage *stage = cls; 417 struct TALER_MHD_TypstContext *tc = stage->tc; 418 419 stage->cwh = NULL; 420 GNUNET_OS_process_destroy (stage->proc); 421 stage->proc = NULL; 422 switch (type) 423 { 424 case GNUNET_OS_PROCESS_UNKNOWN: 425 GNUNET_assert (0); 426 return; 427 case GNUNET_OS_PROCESS_RUNNING: 428 /* we should not get this notification */ 429 GNUNET_break (0); 430 return; 431 case GNUNET_OS_PROCESS_STOPPED: 432 /* Someone is SIGSTOPing our helper!? */ 433 GNUNET_break (0); 434 return; 435 case GNUNET_OS_PROCESS_EXITED: 436 if (0 != exit_code) 437 { 438 char err[128]; 439 440 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 441 "typst exited with status %d\n", 442 (int) exit_code); 443 GNUNET_snprintf (err, 444 sizeof (err), 445 "Typst exited with status %d", 446 (int) exit_code); 447 typst_context_fail (tc, 448 TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE, 449 err); 450 GNUNET_assert (NULL == tc->t); 451 tc->t = GNUNET_SCHEDULER_add_now (&cancel_async, 452 tc); 453 return; 454 } 455 break; 456 case GNUNET_OS_PROCESS_SIGNALED: 457 { 458 char err[128]; 459 460 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 461 "typst died with signal %d\n", 462 (int) exit_code); 463 GNUNET_snprintf (err, 464 sizeof (err), 465 "Typst died with signal %d", 466 (int) exit_code); 467 typst_context_fail (tc, 468 TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH, 469 err); 470 GNUNET_assert (NULL == tc->t); 471 tc->t = GNUNET_SCHEDULER_add_now (&cancel_async, 472 tc); 473 return; 474 } 475 break; 476 } 477 tc->active_stages--; 478 if (NULL != stage->proc) 479 { 480 GNUNET_OS_process_destroy (stage->proc); 481 stage->proc = NULL; 482 } 483 if (0 != tc->active_stages) 484 return; 485 GNUNET_assert (NULL == tc->t); 486 tc->t = GNUNET_SCHEDULER_add_now (&complete_response, 487 tc); 488 } 489 490 491 /** 492 * Setup typst stage to produce one of the PDF inputs. 493 * 494 * @param[out] stage initialized stage data 495 * @param i index of the stage 496 * @param tmpdir where to place temporary files 497 * @param template_path where to find templates 498 * @param doc input document specification 499 * @return true on success 500 */ 501 static bool 502 setup_stage (struct TypstStage *stage, 503 unsigned int i, 504 const char *tmpdir, 505 const char *template_path, 506 const struct TALER_MHD_TypstDocument *doc) 507 { 508 char *input; 509 510 if (NULL == doc->form_name) 511 { 512 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 513 "Stage %u: Dumping inlined PDF attachment\n", 514 i); 515 return inline_pdf_stage (stage, 516 tmpdir, 517 doc->data); 518 } 519 520 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 521 "Stage %u: Handling form %s\n", 522 i, 523 doc->form_name); 524 525 /* Setup inputs */ 526 { 527 char *dirname; 528 529 GNUNET_asprintf (&dirname, 530 "%s/%u/", 531 tmpdir, 532 i); 533 if (GNUNET_OK != 534 GNUNET_DISK_directory_create (dirname)) 535 { 536 GNUNET_free (dirname); 537 return false; 538 } 539 GNUNET_free (dirname); 540 } 541 542 /* Setup data input */ 543 { 544 char *jfn; 545 546 GNUNET_asprintf (&jfn, 547 "%s/%u/input.json", 548 tmpdir, 549 i); 550 if (0 != 551 json_dump_file (doc->data, 552 jfn, 553 JSON_INDENT (2) 554 // JSON_COMPACT 555 )) 556 { 557 GNUNET_break (0); 558 GNUNET_free (jfn); 559 return false; 560 } 561 GNUNET_free (jfn); 562 } 563 564 /* setup output file name */ 565 GNUNET_asprintf (&stage->filename, 566 "%s/%u/input.pdf", 567 tmpdir, 568 i); 569 570 /* setup main input Typst file */ 571 { 572 char *intyp; 573 char *template_fn; 574 575 GNUNET_asprintf (&template_fn, 576 "%s%s.typ", 577 template_path, 578 doc->form_name); 579 if (GNUNET_YES != 580 GNUNET_DISK_file_test_read (template_fn)) 581 { 582 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 583 "access", 584 template_fn); 585 GNUNET_free (template_fn); 586 return false; 587 } 588 GNUNET_asprintf (&intyp, 589 "#import \"%s\": form\n" 590 "#form(json(\"%s/%u/input.json\"))\n", 591 template_fn, 592 tmpdir, 593 i); 594 GNUNET_asprintf (&input, 595 "%s/%u/input.typ", 596 tmpdir, 597 i); 598 if (GNUNET_OK != 599 GNUNET_DISK_fn_write (input, 600 intyp, 601 strlen (intyp), 602 GNUNET_DISK_PERM_USER_READ)) 603 { 604 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 605 "write", 606 input); 607 GNUNET_free (input); 608 GNUNET_free (intyp); 609 GNUNET_free (template_fn); 610 return false; 611 } 612 GNUNET_free (template_fn); 613 GNUNET_free (intyp); 614 } 615 616 /* now setup typst invocation */ 617 { 618 const char *argv[6]; 619 620 argv[0] = "typst"; 621 argv[1] = "compile"; 622 /* This deliberately breaks the typst sandbox. Why? Because 623 they suck and do not support multiple roots, but we have 624 dynamic data in /tmp and resources outside of /tmp and 625 copying all the time is also bad. Typst should really 626 support multiple roots. */ 627 argv[2] = "--root"; 628 argv[3] = "/"; 629 argv[4] = input; 630 argv[5] = NULL; 631 stage->proc = GNUNET_OS_start_process_vap ( 632 GNUNET_OS_INHERIT_STD_ERR, 633 NULL, 634 NULL, 635 NULL, 636 "typst", 637 (char **) argv); 638 if (NULL == stage->proc) 639 { 640 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 641 "fork"); 642 GNUNET_free (input); 643 return false; 644 } 645 GNUNET_free (input); 646 stage->tc->active_stages++; 647 stage->cwh = GNUNET_wait_child (stage->proc, 648 &typst_done_cb, 649 stage); 650 GNUNET_assert (NULL != stage->cwh); 651 } 652 return true; 653 } 654 655 656 struct TALER_MHD_TypstContext * 657 TALER_MHD_typst ( 658 const struct GNUNET_CONFIGURATION_Handle *cfg, 659 bool remove_on_exit, 660 const char *cfg_section_name, 661 unsigned int num_documents, 662 const struct TALER_MHD_TypstDocument docs[static num_documents], 663 TALER_MHD_TypstResultCallback cb, 664 void *cb_cls) 665 { 666 static enum GNUNET_GenericReturnValue once = GNUNET_NO; 667 struct TALER_MHD_TypstContext *tc; 668 669 switch (once) 670 { 671 case GNUNET_OK: 672 break; 673 case GNUNET_NO: 674 if (GNUNET_SYSERR == 675 GNUNET_OS_check_helper_binary ("typst", 676 false, 677 NULL)) 678 { 679 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 680 "`typst' command not found\n"); 681 once = GNUNET_SYSERR; 682 return NULL; 683 } 684 if (GNUNET_SYSERR == 685 GNUNET_OS_check_helper_binary ("pdftk", 686 false, 687 NULL)) 688 { 689 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 690 "`pdftk' command not found\n"); 691 once = GNUNET_SYSERR; 692 return NULL; 693 } 694 once = GNUNET_OK; 695 break; 696 case GNUNET_SYSERR: 697 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 698 "PDF generation initialization failed before, not even trying again\n"); 699 return NULL; 700 } 701 tc = GNUNET_new (struct TALER_MHD_TypstContext); 702 tc->tmpdir = GNUNET_strdup ("/tmp/taler-typst-XXXXXX"); 703 tc->remove_on_exit = remove_on_exit; 704 tc->cb = cb; 705 tc->cb_cls = cb_cls; 706 if (NULL == mkdtemp (tc->tmpdir)) 707 { 708 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, 709 "mkdtemp", 710 tc->tmpdir); 711 GNUNET_free (tc->tmpdir); 712 TALER_MHD_typst_cancel (tc); 713 return NULL; 714 } 715 GNUNET_asprintf (&tc->output_file, 716 "%s/final.pdf", 717 tc->tmpdir); 718 719 /* setup typst stages */ 720 { 721 char *template_path; 722 723 if (GNUNET_OK != 724 GNUNET_CONFIGURATION_get_value_filename (cfg, 725 cfg_section_name, 726 "TYPST_TEMPLATES", 727 &template_path)) 728 { 729 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 730 cfg_section_name, 731 "TYPST_TEMPLATES"); 732 TALER_MHD_typst_cancel (tc); 733 return NULL; 734 } 735 tc->stages = GNUNET_new_array (num_documents, 736 struct TypstStage); 737 tc->num_stages = num_documents; 738 for (unsigned int i = 0; i<num_documents; i++) 739 { 740 tc->stages[i].tc = tc; 741 if (! setup_stage (&tc->stages[i], 742 i, 743 tc->tmpdir, 744 template_path, 745 &docs[i])) 746 { 747 char err[128]; 748 749 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 750 "Typst setup failed on stage %u\n", 751 i); 752 GNUNET_snprintf (err, 753 sizeof (err), 754 "Typst setup failed on stage %u", 755 i); 756 typst_context_fail (tc, 757 TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE, 758 err); 759 TALER_MHD_typst_cancel (tc); 760 return NULL; 761 } 762 } 763 GNUNET_free (template_path); 764 } 765 if (0 == tc->active_stages) 766 { 767 tc->t = GNUNET_SCHEDULER_add_now (&complete_response, 768 tc); 769 } 770 return tc; 771 } 772 773 774 struct MHD_Response * 775 TALER_MHD_response_from_pdf_file (const char *filename) 776 { 777 struct MHD_Response *resp; 778 struct stat s; 779 int fd; 780 781 fd = open (filename, 782 O_RDONLY); 783 if (-1 == fd) 784 { 785 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 786 "open", 787 filename); 788 return NULL; 789 } 790 if (0 != 791 fstat (fd, 792 &s)) 793 { 794 GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, 795 "fstat", 796 filename); 797 GNUNET_assert (0 == close (fd)); 798 return NULL; 799 } 800 resp = MHD_create_response_from_fd (s.st_size, 801 fd); 802 TALER_MHD_add_global_headers (resp, 803 false); 804 GNUNET_break (MHD_YES == 805 MHD_add_response_header (resp, 806 MHD_HTTP_HEADER_CONTENT_TYPE, 807 "application/pdf")); 808 return resp; 809 }