Line data Source code
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 "platform.h" /* UNNECESSARY? */
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_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 : * Project data to use for Typst.
62 : */
63 : const struct GNUNET_OS_ProjectData *pd;
64 :
65 : /**
66 : * Directory where we create temporary files (or FIFOs) for the IPC.
67 : */
68 : char *tmpdir;
69 :
70 : /**
71 : * Array of stages producing PDFs to be combined.
72 : */
73 : struct TypstStage *stages;
74 :
75 : /**
76 : * Handle for pdftk combining the various PDFs.
77 : */
78 : struct GNUNET_Process *proc;
79 :
80 : /**
81 : * Handle to wait for @e proc to complete.
82 : */
83 : struct GNUNET_ChildWaitHandle *cwh;
84 :
85 : /**
86 : * Callback to call on the final result.
87 : */
88 : TALER_MHD_TypstResultCallback cb;
89 :
90 : /**
91 : * Closure for @e cb
92 : */
93 : void *cb_cls;
94 :
95 : /**
96 : * Task for async work.
97 : */
98 : struct GNUNET_SCHEDULER_Task *t;
99 :
100 : /**
101 : * Name of the final file created by pdftk.
102 : */
103 : char *output_file;
104 :
105 : /**
106 : * Context for fail_async_cb().
107 : */
108 : char *async_hint;
109 :
110 : /**
111 : * Context for fail_async_cb().
112 : */
113 : enum TALER_ErrorCode async_ec;
114 :
115 : /**
116 : * Length of the @e stages array.
117 : */
118 : unsigned int num_stages;
119 :
120 : /**
121 : * Number of still active stages.
122 : */
123 : unsigned int active_stages;
124 :
125 : /**
126 : * Should the directory be removed when done?
127 : */
128 : bool remove_on_exit;
129 : };
130 :
131 :
132 : void
133 0 : TALER_MHD_typst_cancel (struct TALER_MHD_TypstContext *tc)
134 : {
135 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
136 : "Cleaning up TypstContext\n");
137 0 : if (NULL != tc->t)
138 : {
139 0 : GNUNET_SCHEDULER_cancel (tc->t);
140 0 : tc->t = NULL;
141 : }
142 0 : for (unsigned int i = 0; i<tc->num_stages; i++)
143 : {
144 0 : struct TypstStage *stage = &tc->stages[i];
145 :
146 0 : if (NULL != stage->cwh)
147 : {
148 0 : GNUNET_wait_child_cancel (stage->cwh);
149 0 : stage->cwh = NULL;
150 : }
151 0 : if (NULL != stage->proc)
152 : {
153 0 : GNUNET_break (GNUNET_OK ==
154 : GNUNET_process_kill (stage->proc,
155 : SIGKILL));
156 : /* reap the child, nobody else will (we cancelled the cwh) */
157 0 : GNUNET_break (GNUNET_OK ==
158 : GNUNET_process_wait (stage->proc,
159 : true,
160 : NULL,
161 : NULL));
162 0 : GNUNET_process_destroy (stage->proc);
163 0 : stage->proc = NULL;
164 : }
165 0 : GNUNET_free (stage->filename);
166 : }
167 0 : GNUNET_free (tc->stages);
168 0 : if (NULL != tc->cwh)
169 : {
170 0 : GNUNET_wait_child_cancel (tc->cwh);
171 0 : tc->cwh = NULL;
172 : }
173 0 : if (NULL != tc->proc)
174 : {
175 0 : GNUNET_break (GNUNET_OK ==
176 : GNUNET_process_kill (tc->proc,
177 : SIGKILL));
178 0 : GNUNET_break (GNUNET_OK ==
179 : GNUNET_process_wait (tc->proc,
180 : true,
181 : NULL,
182 : NULL));
183 0 : GNUNET_process_destroy (tc->proc);
184 : }
185 0 : GNUNET_free (tc->output_file);
186 0 : if (NULL != tc->tmpdir)
187 : {
188 0 : if (tc->remove_on_exit)
189 0 : GNUNET_DISK_directory_remove (tc->tmpdir);
190 0 : GNUNET_free (tc->tmpdir);
191 : }
192 0 : GNUNET_free (tc);
193 0 : }
194 :
195 :
196 : /**
197 : * Create file in @a tmpdir with one of the PDF inputs.
198 : *
199 : * @param[out] stage initialized stage data
200 : * @param tmpdir where to place temporary files
201 : * @param data input JSON with PDF data
202 : * @return true on success
203 : */
204 : static bool
205 0 : inline_pdf_stage (struct TypstStage *stage,
206 : const char *tmpdir,
207 : const json_t *data)
208 : {
209 0 : const char *str = json_string_value (data);
210 : char *fn;
211 : size_t n;
212 : void *b;
213 : int fd;
214 :
215 0 : if (NULL == str)
216 : {
217 0 : GNUNET_break (0);
218 0 : return false;
219 : }
220 0 : b = NULL;
221 0 : n = GNUNET_STRINGS_base64_decode (str,
222 : strlen (str),
223 : &b);
224 0 : if (NULL == b)
225 : {
226 0 : GNUNET_break (0);
227 0 : return false;
228 : }
229 0 : GNUNET_asprintf (&fn,
230 : "%s/external-",
231 : tmpdir);
232 0 : stage->filename = GNUNET_DISK_mktemp (fn);
233 0 : if (NULL == stage->filename)
234 : {
235 0 : GNUNET_break (0);
236 0 : GNUNET_free (b);
237 0 : GNUNET_free (fn);
238 0 : return false;
239 : }
240 0 : GNUNET_free (fn);
241 0 : fd = open (stage->filename,
242 : O_WRONLY | O_TRUNC,
243 : S_IRUSR | S_IWUSR);
244 0 : if (-1 == fd)
245 : {
246 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
247 : "open",
248 : stage->filename);
249 0 : if (0 != unlink (stage->filename))
250 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
251 : "unlink",
252 : stage->filename);
253 0 : GNUNET_free (b);
254 0 : GNUNET_free (stage->filename);
255 0 : return false;
256 : }
257 :
258 : {
259 0 : size_t off = 0;
260 :
261 0 : while (off < n)
262 : {
263 : ssize_t r;
264 :
265 0 : r = write (fd,
266 0 : b + off,
267 : n - off);
268 0 : if (-1 == r)
269 : {
270 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
271 : "write",
272 : stage->filename);
273 0 : GNUNET_break (0 == close (fd));
274 0 : GNUNET_free (b);
275 0 : GNUNET_free (stage->filename);
276 0 : return false;
277 : }
278 0 : off += r;
279 : }
280 : }
281 0 : GNUNET_break (0 == close (fd));
282 0 : GNUNET_free (b);
283 0 : return true;
284 : }
285 :
286 :
287 : /**
288 : * Generate a response for @a tc indicating an error of type @a ec.
289 : *
290 : * @param[in,out] tc context to fail
291 : * @param ec error code to return
292 : * @param hint hint text to return
293 : */
294 : static void
295 0 : typst_context_fail (struct TALER_MHD_TypstContext *tc,
296 : enum TALER_ErrorCode ec,
297 : const char *hint)
298 : {
299 0 : struct TALER_MHD_TypstResponse resp = {
300 : .ec = ec,
301 : .details.hint = hint
302 : };
303 :
304 0 : if (NULL != tc->cb)
305 : {
306 0 : tc->cb (tc->cb_cls,
307 : &resp);
308 0 : tc->cb = NULL;
309 : }
310 0 : }
311 :
312 :
313 : /**
314 : * Helper task for typst_context_fail_async().
315 : *
316 : * @param cls a `struct TALER_MHD_TypstContext`
317 : */
318 : static void
319 0 : fail_async_cb (void *cls)
320 : {
321 0 : struct TALER_MHD_TypstContext *tc = cls;
322 :
323 0 : tc->t = NULL;
324 0 : typst_context_fail (tc,
325 : tc->async_ec,
326 0 : tc->async_hint);
327 0 : GNUNET_free (tc->async_hint);
328 0 : TALER_MHD_typst_cancel (tc);
329 0 : }
330 :
331 :
332 : /**
333 : * Generate a response for @a tc indicating an error of type @a ec.
334 : *
335 : * @param[in,out] tc context to fail
336 : * @param ec error code to return
337 : * @param hint hint text to return
338 : */
339 : static void
340 0 : typst_context_fail_async (struct TALER_MHD_TypstContext *tc,
341 : enum TALER_ErrorCode ec,
342 : const char *hint)
343 : {
344 0 : tc->async_ec = ec;
345 0 : tc->async_hint = (NULL == hint) ? NULL : GNUNET_strdup (hint);
346 0 : tc->t = GNUNET_SCHEDULER_add_now (&fail_async_cb,
347 : tc);
348 0 : }
349 :
350 :
351 : /**
352 : * Called when the pdftk helper exited.
353 : *
354 : * @param cls our `struct TALER_MHD_TypstContext *`
355 : * @param type type of the process
356 : * @param exit_code status code of the process
357 : */
358 : static void
359 0 : pdftk_done_cb (void *cls,
360 : enum GNUNET_OS_ProcessStatusType type,
361 : long unsigned int exit_code)
362 : {
363 0 : struct TALER_MHD_TypstContext *tc = cls;
364 :
365 0 : tc->cwh = NULL;
366 0 : GNUNET_process_destroy (tc->proc);
367 0 : tc->proc = NULL;
368 0 : switch (type)
369 : {
370 0 : case GNUNET_OS_PROCESS_UNKNOWN:
371 0 : GNUNET_assert (0);
372 : return;
373 0 : case GNUNET_OS_PROCESS_RUNNING:
374 : /* we should not get this notification */
375 0 : GNUNET_break (0);
376 0 : typst_context_fail (tc,
377 : TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH,
378 : "pdftk in unexpected state");
379 0 : break;
380 0 : case GNUNET_OS_PROCESS_STOPPED:
381 : /* Someone is SIGSTOPing our helper!? */
382 0 : GNUNET_break (0);
383 0 : typst_context_fail (tc,
384 : TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH,
385 : "pdftk was stopped");
386 0 : break;
387 0 : case GNUNET_OS_PROCESS_EXITED:
388 0 : if (0 != exit_code)
389 : {
390 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
391 : "pdftk exited with status %d\n",
392 : (int) exit_code);
393 0 : typst_context_fail (tc,
394 : TALER_EC_EXCHANGE_GENERIC_PDFTK_FAILURE,
395 : "pdftk failed");
396 : }
397 : else
398 : {
399 0 : struct TALER_MHD_TypstResponse resp = {
400 : .ec = TALER_EC_NONE,
401 0 : .details.filename = tc->output_file,
402 : };
403 :
404 0 : GNUNET_assert (NULL != tc->cb);
405 0 : tc->cb (tc->cb_cls,
406 : &resp);
407 0 : tc->cb = NULL;
408 : }
409 0 : break;
410 0 : case GNUNET_OS_PROCESS_SIGNALED:
411 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
412 : "pdftk died with signal %d\n",
413 : (int) exit_code);
414 0 : typst_context_fail (tc,
415 : TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH,
416 : "pdftk killed by signal");
417 0 : break;
418 : }
419 0 : TALER_MHD_typst_cancel (tc);
420 : }
421 :
422 :
423 : /**
424 : * Function called once all of the individual stages are done.
425 : * Triggers the pdftk run for @a tc.
426 : *
427 : * @param[in,out] cls a `struct TALER_MHD_TypstContext *` context to run pdftk for
428 : */
429 : static void
430 0 : complete_response (void *cls)
431 0 : {
432 0 : struct TALER_MHD_TypstContext *tc = cls;
433 0 : const char *argv[tc->num_stages + 5];
434 :
435 0 : tc->t = NULL;
436 0 : argv[0] = "pdftk";
437 0 : for (unsigned int i = 0; i<tc->num_stages; i++)
438 0 : argv[i + 1] = tc->stages[i].filename;
439 0 : argv[tc->num_stages + 1] = "cat";
440 0 : argv[tc->num_stages + 2] = "output";
441 0 : argv[tc->num_stages + 3] = tc->output_file;
442 0 : argv[tc->num_stages + 4] = NULL;
443 0 : tc->proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
444 0 : if (GNUNET_OK !=
445 0 : GNUNET_process_run_command_argv (tc->proc,
446 : argv[0],
447 : argv))
448 : {
449 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
450 : "fork");
451 0 : GNUNET_process_destroy (tc->proc);
452 0 : tc->proc = NULL;
453 : /* The caller still holds @a tc until its callback runs,
454 : so we must tell it before freeing @a tc. */
455 0 : typst_context_fail (tc,
456 : TALER_EC_EXCHANGE_GENERIC_PDFTK_FAILURE,
457 : "could not start pdftk");
458 0 : TALER_MHD_typst_cancel (tc);
459 0 : return;
460 : }
461 0 : tc->cwh = GNUNET_wait_child (tc->proc,
462 : &pdftk_done_cb,
463 : tc);
464 0 : GNUNET_assert (NULL != tc->cwh);
465 : }
466 :
467 :
468 : /**
469 : * Cancel typst. Wrapper task to do so asynchronously.
470 : *
471 : * @param[in] cls a `struct TALER_MHD_TypstContext`
472 : */
473 : static void
474 0 : cancel_async (void *cls)
475 : {
476 0 : struct TALER_MHD_TypstContext *tc = cls;
477 :
478 0 : tc->t = NULL;
479 0 : TALER_MHD_typst_cancel (tc);
480 0 : }
481 :
482 :
483 : /**
484 : * Fail the typst context @a tc and clean it up asynchronously.
485 : *
486 : * Note that several stages may fail within the same SIGCHLD batch, in
487 : * which case the clean-up task will already have been scheduled by the
488 : * first failure; we must not schedule it a second time (and must not
489 : * clean up synchronously, as we are called from the child-death
490 : * notification).
491 : *
492 : * @param[in,out] tc context to fail
493 : * @param ec error code to return to the client
494 : * @param hint hint text to return to the client
495 : */
496 : static void
497 0 : stage_fail (struct TALER_MHD_TypstContext *tc,
498 : enum TALER_ErrorCode ec,
499 : const char *hint)
500 : {
501 0 : typst_context_fail (tc,
502 : ec,
503 : hint);
504 0 : if (NULL == tc->t)
505 0 : tc->t = GNUNET_SCHEDULER_add_now (&cancel_async,
506 : tc);
507 0 : }
508 :
509 :
510 : /**
511 : * Called when a typst helper exited.
512 : *
513 : * @param cls our `struct TypstStage *`
514 : * @param type type of the process
515 : * @param exit_code status code of the process
516 : */
517 : static void
518 0 : typst_done_cb (void *cls,
519 : enum GNUNET_OS_ProcessStatusType type,
520 : long unsigned int exit_code)
521 : {
522 0 : struct TypstStage *stage = cls;
523 0 : struct TALER_MHD_TypstContext *tc = stage->tc;
524 :
525 0 : stage->cwh = NULL;
526 0 : GNUNET_process_destroy (stage->proc);
527 0 : stage->proc = NULL;
528 0 : switch (type)
529 : {
530 0 : case GNUNET_OS_PROCESS_UNKNOWN:
531 0 : GNUNET_assert (0);
532 : return;
533 0 : case GNUNET_OS_PROCESS_RUNNING:
534 : /* we should not get this notification */
535 0 : GNUNET_break (0);
536 0 : stage_fail (tc,
537 : TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH,
538 : "Typst in unexpected state");
539 0 : return;
540 0 : case GNUNET_OS_PROCESS_STOPPED:
541 : /* Someone is SIGSTOPing our helper!? */
542 0 : GNUNET_break (0);
543 0 : stage_fail (tc,
544 : TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH,
545 : "Typst was stopped");
546 0 : return;
547 0 : case GNUNET_OS_PROCESS_EXITED:
548 0 : if (0 != exit_code)
549 : {
550 : char err[128];
551 :
552 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
553 : "typst exited with status %d\n",
554 : (int) exit_code);
555 0 : GNUNET_snprintf (err,
556 : sizeof (err),
557 : "Typst exited with status %d",
558 : (int) exit_code);
559 0 : stage_fail (tc,
560 : TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
561 : err);
562 0 : return;
563 : }
564 0 : break;
565 0 : case GNUNET_OS_PROCESS_SIGNALED:
566 : {
567 : char err[128];
568 :
569 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
570 : "typst died with signal %d\n",
571 : (int) exit_code);
572 0 : GNUNET_snprintf (err,
573 : sizeof (err),
574 : "Typst died with signal %d",
575 : (int) exit_code);
576 0 : stage_fail (tc,
577 : TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH,
578 : err);
579 0 : return;
580 : }
581 : break;
582 : }
583 0 : tc->active_stages--;
584 0 : if (NULL != stage->proc)
585 : {
586 0 : GNUNET_process_destroy (stage->proc);
587 0 : stage->proc = NULL;
588 : }
589 0 : if (0 != tc->active_stages)
590 0 : return;
591 0 : if (NULL != tc->t)
592 0 : return; /* clean-up of a failed stage is already scheduled */
593 0 : tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
594 : tc);
595 : }
596 :
597 :
598 : /**
599 : * Check that @a id only consists of characters that are safe to
600 : * interpolate into a Typst import statement (alphanumerics, dots,
601 : * hyphens and underscores). This prevents code/path injection via the form
602 : * name or version.
603 : *
604 : * @param id identifier to validate
605 : * @return true if @a id is non-empty and only uses the safe charset
606 : */
607 : static bool
608 0 : is_valid_form_identifier (const char *id)
609 : {
610 0 : if ( (NULL == id) ||
611 0 : ('\0' == id[0]) )
612 0 : return false;
613 0 : for (const char *p = id; '\0' != *p; p++)
614 : {
615 0 : char c = *p;
616 :
617 0 : if (! ( ( ('a' <= c) && ('z' >= c)) ||
618 0 : ( ('A' <= c) && ('Z' >= c)) ||
619 0 : ( ('0' <= c) && ('9' >= c)) ||
620 0 : ('.' == c) ||
621 : ('-' == c) ||
622 : ('_' == c) ) )
623 0 : return false;
624 : }
625 0 : return true;
626 : }
627 :
628 :
629 : /**
630 : * Setup typst stage to produce one of the PDF inputs.
631 : *
632 : * @param[out] stage initialized stage data
633 : * @param i index of the stage
634 : * @param tmpdir where to place temporary files
635 : * @param template_path where to find templates
636 : * @param doc input document specification
637 : * @return true on success
638 : */
639 : static bool
640 0 : setup_stage (struct TypstStage *stage,
641 : unsigned int i,
642 : const char *tmpdir,
643 : const char *template_path,
644 : const struct TALER_MHD_TypstDocument *doc)
645 : {
646 0 : struct TALER_MHD_TypstContext *tc = stage->tc;
647 : char *input;
648 : bool is_dot_typ;
649 :
650 0 : if (NULL == doc->form_name)
651 : {
652 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
653 : "Stage %u: Dumping inlined PDF attachment\n",
654 : i);
655 0 : return inline_pdf_stage (stage,
656 : tmpdir,
657 0 : doc->data);
658 : }
659 :
660 0 : if (! is_valid_form_identifier (doc->form_name))
661 : {
662 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
663 : "Stage %u: Invalid form name `%s'\n",
664 : i,
665 : doc->form_name);
666 0 : return false;
667 : }
668 0 : if ( (NULL != doc->form_version) &&
669 0 : (! is_valid_form_identifier (doc->form_version)) )
670 : {
671 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
672 : "Stage %u: Invalid form version `%s'\n",
673 : i,
674 : doc->form_version);
675 0 : return false;
676 : }
677 :
678 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
679 : "Stage %u: Handling form %s\n",
680 : i,
681 : doc->form_name);
682 :
683 : /* Setup inputs */
684 : {
685 : char *dirname;
686 :
687 0 : GNUNET_asprintf (&dirname,
688 : "%s/%u/",
689 : tmpdir,
690 : i);
691 0 : if (GNUNET_OK !=
692 0 : GNUNET_DISK_directory_create (dirname))
693 : {
694 0 : GNUNET_free (dirname);
695 0 : return false;
696 : }
697 0 : GNUNET_free (dirname);
698 : }
699 :
700 : /* Setup data input */
701 : {
702 : char *jfn;
703 :
704 0 : GNUNET_asprintf (&jfn,
705 : "%s/%u/input.json",
706 : tmpdir,
707 : i);
708 0 : if (0 !=
709 0 : json_dump_file (doc->data,
710 : jfn,
711 : JSON_INDENT (2)))
712 : {
713 0 : GNUNET_break (0);
714 0 : GNUNET_free (jfn);
715 0 : return false;
716 : }
717 0 : GNUNET_free (jfn);
718 : }
719 :
720 : /* setup output file name */
721 0 : GNUNET_asprintf (&stage->filename,
722 : "%s/%u/input.pdf",
723 : tmpdir,
724 : i);
725 :
726 : /* setup main input Typst file */
727 : {
728 : char *intyp;
729 0 : size_t slen = strlen (doc->form_name);
730 :
731 0 : is_dot_typ = ( (slen > 4) &&
732 0 : (0 == memcmp (&doc->form_name[slen - 4],
733 : ".typ",
734 : 4)) );
735 : /* We do not append the ":$VERSION" if a filename ending with ".typ"
736 : is given. Otherwise we append the version, or ":0.0.0" if no
737 : explicit version is given. */
738 0 : GNUNET_asprintf (&intyp,
739 : "#import \"%s/%s%s%s\": form\n"
740 : "#form(json(\"input.json\"))\n",
741 : template_path,
742 0 : doc->form_name,
743 : is_dot_typ ? "" : ":",
744 : is_dot_typ
745 : ? ""
746 0 : : ( (NULL == doc->form_version)
747 : ? "0.0.0"
748 0 : : doc->form_version));
749 0 : GNUNET_asprintf (&input,
750 : "%s/%u/input.typ",
751 : tmpdir,
752 : i);
753 0 : if (GNUNET_OK !=
754 0 : GNUNET_DISK_fn_write (input,
755 : intyp,
756 : strlen (intyp),
757 : GNUNET_DISK_PERM_USER_READ))
758 : {
759 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
760 : "write",
761 : input);
762 0 : GNUNET_free (input);
763 0 : GNUNET_free (intyp);
764 0 : return false;
765 : }
766 0 : GNUNET_free (intyp);
767 : }
768 :
769 : /* now setup typst invocation */
770 : {
771 : const char *argv[6];
772 :
773 0 : if (is_dot_typ)
774 : {
775 : /* This deliberately breaks the typst sandbox. Why? Because Typst sucks
776 : and does not support multiple roots, but here we have dynamic data in
777 : /tmp and a style file outside of /tmp (and copying is also not
778 : practical as we don't know what all to copy). Typst should really
779 : support multiple roots. Anyway, in production this path should not
780 : happen, because there we use Typst packages. */
781 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
782 : "Bypassing Typst sandbox. You should use Typst packages instead of `%s'.\n",
783 : doc->form_name);
784 0 : argv[0] = "typst";
785 0 : argv[1] = "compile";
786 0 : argv[2] = "--root";
787 0 : argv[3] = "/";
788 0 : argv[4] = input;
789 0 : argv[5] = NULL;
790 : }
791 : else
792 : {
793 0 : argv[0] = "typst";
794 0 : argv[1] = "compile";
795 0 : argv[2] = input;
796 0 : argv[3] = NULL;
797 : }
798 0 : stage->proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
799 : {
800 : char *datadir;
801 :
802 0 : datadir = GNUNET_OS_installation_get_path (
803 : tc->pd,
804 : GNUNET_OS_IPK_DATADIR);
805 0 : GNUNET_assert (GNUNET_OK ==
806 : GNUNET_process_set_options (
807 : stage->proc,
808 : GNUNET_process_option_set_environment ("XDG_DATA_HOME",
809 : datadir)));
810 0 : GNUNET_free (datadir);
811 : }
812 0 : if (GNUNET_OK !=
813 0 : GNUNET_process_run_command_argv (stage->proc,
814 : "typst",
815 : argv))
816 : {
817 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
818 : "fork");
819 0 : GNUNET_free (input);
820 0 : GNUNET_process_destroy (stage->proc);
821 0 : stage->proc = NULL;
822 0 : return false;
823 : }
824 0 : GNUNET_free (input);
825 0 : tc->active_stages++;
826 0 : stage->cwh = GNUNET_wait_child (stage->proc,
827 : &typst_done_cb,
828 : stage);
829 0 : GNUNET_assert (NULL != stage->cwh);
830 : }
831 0 : return true;
832 : }
833 :
834 :
835 : struct TALER_MHD_TypstContext *
836 1 : TALER_MHD_typst (
837 : const struct GNUNET_OS_ProjectData *pd,
838 : const struct GNUNET_CONFIGURATION_Handle *cfg,
839 : bool remove_on_exit,
840 : const char *cfg_section_name,
841 : unsigned int num_documents,
842 : const struct TALER_MHD_TypstDocument docs[static num_documents],
843 : TALER_MHD_TypstResultCallback cb,
844 : void *cb_cls)
845 1 : {
846 : static enum GNUNET_GenericReturnValue once = GNUNET_NO;
847 : struct TALER_MHD_TypstContext *tc;
848 :
849 1 : switch (once)
850 : {
851 0 : case GNUNET_OK:
852 0 : break;
853 1 : case GNUNET_NO:
854 1 : if (GNUNET_SYSERR ==
855 1 : GNUNET_OS_check_helper_binary ("typst",
856 : false,
857 : NULL))
858 : {
859 1 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
860 : "`typst' command not found\n");
861 1 : once = GNUNET_SYSERR;
862 1 : return NULL;
863 : }
864 0 : if (GNUNET_SYSERR ==
865 0 : GNUNET_OS_check_helper_binary ("pdftk",
866 : false,
867 : NULL))
868 : {
869 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
870 : "`pdftk' command not found\n");
871 0 : once = GNUNET_SYSERR;
872 0 : return NULL;
873 : }
874 0 : once = GNUNET_OK;
875 0 : break;
876 0 : case GNUNET_SYSERR:
877 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
878 : "PDF generation initialization failed before, not even trying again\n");
879 0 : return NULL;
880 : }
881 0 : tc = GNUNET_new (struct TALER_MHD_TypstContext);
882 0 : tc->pd = pd;
883 0 : tc->tmpdir = GNUNET_strdup ("/tmp/taler-typst-XXXXXX");
884 0 : tc->remove_on_exit = remove_on_exit;
885 0 : tc->cb = cb;
886 0 : tc->cb_cls = cb_cls;
887 0 : if (NULL == mkdtemp (tc->tmpdir))
888 : {
889 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
890 : "mkdtemp",
891 : tc->tmpdir);
892 0 : GNUNET_free (tc->tmpdir);
893 0 : TALER_MHD_typst_cancel (tc);
894 0 : return NULL;
895 : }
896 0 : GNUNET_asprintf (&tc->output_file,
897 : "%s/final.pdf",
898 : tc->tmpdir);
899 :
900 : /* setup typst stages */
901 : {
902 : char *template_path;
903 :
904 0 : if (GNUNET_OK !=
905 0 : GNUNET_CONFIGURATION_get_value_string (cfg,
906 : cfg_section_name,
907 : "TYPST_TEMPLATES",
908 : &template_path))
909 : {
910 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
911 : cfg_section_name,
912 : "TYPST_TEMPLATES");
913 0 : TALER_MHD_typst_cancel (tc);
914 0 : return NULL;
915 : }
916 0 : if ('@' != template_path[0])
917 0 : template_path = GNUNET_CONFIGURATION_expand_dollar (cfg,
918 : template_path);
919 0 : tc->stages = GNUNET_new_array (num_documents,
920 : struct TypstStage);
921 0 : tc->num_stages = num_documents;
922 0 : for (unsigned int i = 0; i<num_documents; i++)
923 : {
924 0 : tc->stages[i].tc = tc;
925 0 : if (! setup_stage (&tc->stages[i],
926 : i,
927 0 : tc->tmpdir,
928 : template_path,
929 0 : &docs[i]))
930 : {
931 : char err[128];
932 :
933 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
934 : "Typst setup failed on stage %u\n",
935 : i);
936 0 : GNUNET_snprintf (err,
937 : sizeof (err),
938 : "Typst setup failed on stage %u",
939 : i);
940 0 : typst_context_fail_async (tc,
941 : TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
942 : err);
943 0 : GNUNET_free (template_path);
944 0 : return tc;
945 : }
946 : }
947 0 : GNUNET_free (template_path);
948 : }
949 0 : if (0 == tc->active_stages)
950 : {
951 0 : tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
952 : tc);
953 : }
954 0 : return tc;
955 : }
956 :
957 :
958 : struct MHD_Response *
959 0 : TALER_MHD_response_from_pdf_file (const char *filename)
960 : {
961 : struct MHD_Response *resp;
962 : struct stat s;
963 : int fd;
964 :
965 0 : fd = open (filename,
966 : O_RDONLY);
967 0 : if (-1 == fd)
968 : {
969 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
970 : "open",
971 : filename);
972 0 : return NULL;
973 : }
974 0 : if (0 !=
975 0 : fstat (fd,
976 : &s))
977 : {
978 0 : GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
979 : "fstat",
980 : filename);
981 0 : GNUNET_assert (0 == close (fd));
982 0 : return NULL;
983 : }
984 0 : resp = MHD_create_response_from_fd (s.st_size,
985 : fd);
986 0 : if (NULL == resp)
987 : {
988 0 : GNUNET_break (0);
989 0 : GNUNET_assert (0 == close (fd));
990 0 : return NULL;
991 : }
992 0 : TALER_MHD_add_global_headers (resp,
993 : false);
994 0 : GNUNET_break (MHD_YES ==
995 : MHD_add_response_header (resp,
996 : MHD_HTTP_HEADER_CONTENT_TYPE,
997 : "application/pdf"));
998 0 : return resp;
999 : }
|