exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

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 }