merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-webhook.c (16965B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2023 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero 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 Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file taler-merchant-webhook.c
     18  * @brief Process that runs webhooks triggered by the merchant backend
     19  * @author Priscilla HUANG
     20  */
     21 #include "platform.h"
     22 #include "microhttpd.h"
     23 #include <gnunet/gnunet_util_lib.h>
     24 #include <jansson.h>
     25 #include <pthread.h>
     26 #include "taler_merchant_util.h"
     27 #include "taler_merchantdb_lib.h"
     28 #include "taler_merchantdb_plugin.h"
     29 #include <taler/taler_dbevents.h>
     30 
     31 
     32 struct WorkResponse
     33 {
     34   struct WorkResponse *next;
     35   struct WorkResponse *prev;
     36   struct GNUNET_CURL_Job *job;
     37   uint64_t webhook_pending_serial;
     38   char *body;
     39   struct curl_slist *job_headers;
     40 };
     41 
     42 
     43 static struct WorkResponse *w_head;
     44 
     45 static struct WorkResponse *w_tail;
     46 
     47 static struct GNUNET_DB_EventHandler *event_handler;
     48 
     49 /**
     50  * The merchant's configuration.
     51  */
     52 static const struct GNUNET_CONFIGURATION_Handle *cfg;
     53 
     54 /**
     55  * Our database plugin.
     56  */
     57 static struct TALER_MERCHANTDB_Plugin *db_plugin;
     58 
     59 /**
     60  * Next task to run, if any.
     61  */
     62 static struct GNUNET_SCHEDULER_Task *task;
     63 
     64 /**
     65  * Handle to the context for interacting with the bank / wire gateway.
     66  */
     67 static struct GNUNET_CURL_Context *ctx;
     68 
     69 /**
     70  * Scheduler context for running the @e ctx.
     71  */
     72 static struct GNUNET_CURL_RescheduleContext *rc;
     73 
     74 /**
     75  * Value to return from main(). 0 on success, non-zero on errors.
     76  */
     77 static int global_ret;
     78 
     79 /**
     80  * #GNUNET_YES if we are in test mode and should exit when idle.
     81  */
     82 static int test_mode;
     83 
     84 
     85 /**
     86  * We're being aborted with CTRL-C (or SIGTERM). Shut down.
     87  *
     88  * @param cls closure
     89  */
     90 static void
     91 shutdown_task (void *cls)
     92 {
     93   struct WorkResponse *w;
     94 
     95   (void) cls;
     96   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     97               "Running shutdown\n");
     98   if (NULL != event_handler)
     99   {
    100     db_plugin->event_listen_cancel (event_handler);
    101     event_handler = NULL;
    102   }
    103   if (NULL != task)
    104   {
    105     GNUNET_SCHEDULER_cancel (task);
    106     task = NULL;
    107   }
    108   while (NULL != (w = w_head))
    109   {
    110     GNUNET_CONTAINER_DLL_remove (w_head,
    111                                  w_tail,
    112                                  w);
    113     GNUNET_CURL_job_cancel (w->job);
    114     curl_slist_free_all (w->job_headers);
    115     GNUNET_free (w->body);
    116     GNUNET_free (w);
    117   }
    118   db_plugin->rollback (db_plugin->cls); /* just in case */
    119   TALER_MERCHANTDB_plugin_unload (db_plugin);
    120   db_plugin = NULL;
    121   cfg = NULL;
    122   if (NULL != ctx)
    123   {
    124     GNUNET_CURL_fini (ctx);
    125     ctx = NULL;
    126   }
    127   if (NULL != rc)
    128   {
    129     GNUNET_CURL_gnunet_rc_destroy (rc);
    130     rc = NULL;
    131   }
    132 }
    133 
    134 
    135 /**
    136  * Select webhook to process.
    137  *
    138  * @param cls NULL
    139  */
    140 static void
    141 select_work (void *cls);
    142 
    143 
    144 /**
    145  * This function is used by the function `pending_webhooks_cb`. According to the response code,
    146  * we delete or update the webhook.
    147  *
    148  * @param cls closure
    149  * @param response_code HTTP response code from server, 0 on hard error
    150  * @param body http body of the response
    151  * @param body_size number of bytes in @a body
    152  */
    153 static void
    154 handle_webhook_response (void *cls,
    155                          long response_code,
    156                          const void *body,
    157                          size_t body_size)
    158 {
    159   struct WorkResponse *w = cls;
    160 
    161   (void) body;
    162   (void) body_size;
    163   w->job = NULL;
    164   GNUNET_CONTAINER_DLL_remove (w_head,
    165                                w_tail,
    166                                w);
    167   GNUNET_free (w->body);
    168   curl_slist_free_all (w->job_headers);
    169   if (NULL == w_head)
    170     task = GNUNET_SCHEDULER_add_now (&select_work,
    171                                      NULL);
    172   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    173               "Webhook %llu returned with status %ld\n",
    174               (unsigned long long) w->webhook_pending_serial,
    175               response_code);
    176   if (2 == response_code / 100) /* any 2xx http status code is OK! */
    177   {
    178     enum GNUNET_DB_QueryStatus qs;
    179 
    180     qs = db_plugin->delete_pending_webhook (db_plugin->cls,
    181                                             w->webhook_pending_serial);
    182     GNUNET_free (w);
    183     switch (qs)
    184     {
    185     case GNUNET_DB_STATUS_HARD_ERROR:
    186     case GNUNET_DB_STATUS_SOFT_ERROR:
    187       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    188                   "Failed to delete webhook, delete returned: %d\n",
    189                   qs);
    190       global_ret = EXIT_FAILURE;
    191       GNUNET_SCHEDULER_shutdown ();
    192       return;
    193     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    194       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    195                   "Delete returned: %d\n",
    196                   qs);
    197       return;
    198     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    199       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    200                   "Delete returned: %d\n",
    201                   qs);
    202       return;
    203     }
    204     GNUNET_assert (0);
    205   }
    206 
    207   {
    208     struct GNUNET_TIME_Relative next_attempt;
    209     enum GNUNET_DB_QueryStatus qs;
    210     switch (response_code)
    211     {
    212     case MHD_HTTP_BAD_REQUEST:
    213       next_attempt = GNUNET_TIME_UNIT_FOREVER_REL;   // never try again
    214       break;
    215     case MHD_HTTP_INTERNAL_SERVER_ERROR:
    216       next_attempt = GNUNET_TIME_UNIT_MINUTES;
    217       break;
    218     case MHD_HTTP_FORBIDDEN:
    219       next_attempt = GNUNET_TIME_UNIT_MINUTES;
    220       break;
    221     default:
    222       next_attempt = GNUNET_TIME_UNIT_HOURS;
    223       break;
    224     }
    225     qs = db_plugin->update_pending_webhook (db_plugin->cls,
    226                                             w->webhook_pending_serial,
    227                                             GNUNET_TIME_relative_to_absolute (
    228                                               next_attempt));
    229     GNUNET_free (w);
    230     switch (qs)
    231     {
    232     case GNUNET_DB_STATUS_HARD_ERROR:
    233     case GNUNET_DB_STATUS_SOFT_ERROR:
    234       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    235                   "Failed to update pending webhook to next in %s Rval: %d\n",
    236                   GNUNET_TIME_relative2s (next_attempt,
    237                                           true),
    238                   qs);
    239       global_ret = EXIT_FAILURE;
    240       GNUNET_SCHEDULER_shutdown ();
    241       return;
    242     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    243       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    244                   "Next in %s Rval: %d\n",
    245                   GNUNET_TIME_relative2s (next_attempt, true),
    246                   qs);
    247       return;
    248     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    249       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    250                   "Next in %s Rval: %d\n",
    251                   GNUNET_TIME_relative2s (next_attempt, true),
    252                   qs);
    253       return;
    254     }
    255     GNUNET_assert (0);
    256   }
    257 }
    258 
    259 
    260 /**
    261  * Typically called by `select_work`.
    262  *
    263  * @param cls a `json_t *` JSON array to build
    264  * @param webhook_pending_serial reference to the configured webhook template.
    265  * @param next_attempt is the time we should make the next request to the webhook.
    266  * @param retries how often have we tried this request to the webhook.
    267  * @param url to make request to
    268  * @param http_method use for the webhook
    269  * @param header of the webhook
    270  * @param body of the webhook
    271  */
    272 static void
    273 pending_webhooks_cb (void *cls,
    274                      uint64_t webhook_pending_serial,
    275                      struct GNUNET_TIME_Absolute next_attempt,
    276                      uint32_t retries,
    277                      const char *url,
    278                      const char *http_method,
    279                      const char *header,
    280                      const char *body)
    281 {
    282   struct WorkResponse *w = GNUNET_new (struct WorkResponse);
    283   CURL *eh;
    284   struct curl_slist *job_headers = NULL;
    285 
    286   (void) retries;
    287   (void) next_attempt;
    288   (void) cls;
    289   GNUNET_CONTAINER_DLL_insert (w_head,
    290                                w_tail,
    291                                w);
    292   w->webhook_pending_serial = webhook_pending_serial;
    293   eh = curl_easy_init ();
    294   GNUNET_assert (NULL != eh);
    295   GNUNET_assert (CURLE_OK ==
    296                  curl_easy_setopt (eh,
    297                                    CURLOPT_CUSTOMREQUEST,
    298                                    http_method));
    299   GNUNET_assert (CURLE_OK ==
    300                  curl_easy_setopt (eh,
    301                                    CURLOPT_URL,
    302                                    url));
    303   GNUNET_assert (CURLE_OK ==
    304                  curl_easy_setopt (eh,
    305                                    CURLOPT_VERBOSE,
    306                                    0L));
    307 
    308   /* conversion body data */
    309   if (NULL != body)
    310   {
    311     w->body = GNUNET_strdup (body);
    312     GNUNET_assert (CURLE_OK ==
    313                    curl_easy_setopt (eh,
    314                                      CURLOPT_POSTFIELDS,
    315                                      w->body));
    316   }
    317   /* conversion header to job_headers data */
    318   if (NULL != header)
    319   {
    320     char *header_copy = GNUNET_strdup (header);
    321 
    322     for (const char *tok = strtok (header_copy, "\n");
    323          NULL != tok;
    324          tok = strtok (NULL, "\n"))
    325     {
    326       // extract all Key: value from 'header_copy'!
    327       job_headers = curl_slist_append (job_headers,
    328                                        tok);
    329     }
    330     GNUNET_free (header_copy);
    331     GNUNET_assert (CURLE_OK ==
    332                    curl_easy_setopt (eh,
    333                                      CURLOPT_HTTPHEADER,
    334                                      job_headers));
    335     w->job_headers = job_headers;
    336   }
    337   GNUNET_assert (CURLE_OK ==
    338                  curl_easy_setopt (eh,
    339                                    CURLOPT_MAXREDIRS,
    340                                    5));
    341   GNUNET_assert (CURLE_OK ==
    342                  curl_easy_setopt (eh,
    343                                    CURLOPT_FOLLOWLOCATION,
    344                                    1));
    345 
    346   w->job = GNUNET_CURL_job_add_raw (ctx,
    347                                     eh,
    348                                     job_headers,
    349                                     &handle_webhook_response,
    350                                     w);
    351   if (NULL == w->job)
    352   {
    353     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    354                 "Failed to start the curl job for pending webhook #%llu\n",
    355                 (unsigned long long) webhook_pending_serial);
    356     curl_slist_free_all (w->job_headers);
    357     GNUNET_free (w->body);
    358     GNUNET_CONTAINER_DLL_remove (w_head,
    359                                  w_tail,
    360                                  w);
    361     GNUNET_free (w);
    362     GNUNET_SCHEDULER_shutdown ();
    363     return;
    364   }
    365 }
    366 
    367 
    368 /**
    369  * Function called on events received from Postgres.
    370  *
    371  * @param cls closure, NULL
    372  * @param extra additional event data provided
    373  * @param extra_size number of bytes in @a extra
    374  */
    375 static void
    376 db_notify (void *cls,
    377            const void *extra,
    378            size_t extra_size)
    379 {
    380   (void) cls;
    381   (void) extra;
    382   (void) extra_size;
    383 
    384   GNUNET_assert (NULL != task);
    385   GNUNET_SCHEDULER_cancel (task);
    386   task = GNUNET_SCHEDULER_add_now (&select_work,
    387                                    NULL);
    388 }
    389 
    390 
    391 /**
    392  * Typically called by `select_work`.
    393  *
    394  * @param cls a `json_t *` JSON array to build
    395  * @param webhook_pending_serial reference to the configured webhook template.
    396  * @param next_attempt is the time we should make the next request to the webhook.
    397  * @param retries how often have we tried this request to the webhook.
    398  * @param url to make request to
    399  * @param http_method use for the webhook
    400  * @param header of the webhook
    401  * @param body of the webhook
    402  */
    403 static void
    404 future_webhook_cb (void *cls,
    405                    uint64_t webhook_pending_serial,
    406                    struct GNUNET_TIME_Absolute next_attempt,
    407                    uint32_t retries,
    408                    const char *url,
    409                    const char *http_method,
    410                    const char *header,
    411                    const char *body)
    412 {
    413   (void) webhook_pending_serial;
    414   (void) retries;
    415   (void) url;
    416   (void) http_method;
    417   (void) header;
    418   (void) body;
    419 
    420   task = GNUNET_SCHEDULER_add_at (next_attempt,
    421                                   &select_work,
    422                                   NULL);
    423 }
    424 
    425 
    426 static void
    427 select_work (void *cls)
    428 {
    429   enum GNUNET_DB_QueryStatus qs;
    430   struct GNUNET_TIME_Relative rel;
    431 
    432   (void) cls;
    433   task = NULL;
    434   db_plugin->preflight (db_plugin->cls);
    435   qs = db_plugin->lookup_pending_webhooks (db_plugin->cls,
    436                                            &pending_webhooks_cb,
    437                                            NULL);
    438   switch (qs)
    439   {
    440   case GNUNET_DB_STATUS_HARD_ERROR:
    441   case GNUNET_DB_STATUS_SOFT_ERROR:
    442     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    443                 "Failed to lookup pending webhooks!\n");
    444     global_ret = EXIT_FAILURE;
    445     GNUNET_SCHEDULER_shutdown ();
    446     return;
    447   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    448     if (test_mode)
    449     {
    450       GNUNET_SCHEDULER_shutdown ();
    451       return;
    452     }
    453     qs = db_plugin->lookup_future_webhook (db_plugin->cls,
    454                                            &future_webhook_cb,
    455                                            NULL);
    456     switch (qs)
    457     {
    458     case GNUNET_DB_STATUS_HARD_ERROR:
    459     case GNUNET_DB_STATUS_SOFT_ERROR:
    460       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    461                   "Failed to lookup future webhook!\n");
    462       global_ret = EXIT_FAILURE;
    463       GNUNET_SCHEDULER_shutdown ();
    464       return;
    465     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    466       return;
    467     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    468       /* wait 5 min */
    469       /* Note: this should not even be necessary if all webhooks
    470          use the events properly... */
    471       rel = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 5);
    472       task = GNUNET_SCHEDULER_add_delayed (rel,
    473                                            &select_work,
    474                                            NULL);
    475       return;
    476     }
    477   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    478   default:
    479     return; // wait for completion, then select more work.
    480   }
    481 }
    482 
    483 
    484 /**
    485  * First task.
    486  *
    487  * @param cls closure, NULL
    488  * @param args remaining command-line arguments
    489  * @param cfgfile name of the configuration file used (for saving, can be NULL!)
    490  * @param c configuration
    491  */
    492 static void
    493 run (void *cls,
    494      char *const *args,
    495      const char *cfgfile,
    496      const struct GNUNET_CONFIGURATION_Handle *c)
    497 {
    498   (void) args;
    499   (void) cfgfile;
    500 
    501   cfg = c;
    502   GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
    503                                  NULL);
    504   ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
    505                           &rc);
    506   rc = GNUNET_CURL_gnunet_rc_create (ctx);
    507   if (NULL == ctx)
    508   {
    509     GNUNET_break (0);
    510     GNUNET_SCHEDULER_shutdown ();
    511     global_ret = EXIT_FAILURE;
    512     return;
    513   }
    514   if (NULL ==
    515       (db_plugin = TALER_MERCHANTDB_plugin_load (cfg)))
    516   {
    517     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    518                 "Failed to initialize DB subsystem\n");
    519     GNUNET_SCHEDULER_shutdown ();
    520     global_ret = EXIT_NOTCONFIGURED;
    521     return;
    522   }
    523   if (GNUNET_OK !=
    524       db_plugin->connect (db_plugin->cls))
    525   {
    526     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    527                 "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
    528     GNUNET_SCHEDULER_shutdown ();
    529     global_ret = EXIT_FAILURE;
    530     return;
    531   }
    532   {
    533     struct GNUNET_DB_EventHeaderP es = {
    534       .size = htons (sizeof (es)),
    535       .type = htons (TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING)
    536     };
    537 
    538     event_handler = db_plugin->event_listen (db_plugin->cls,
    539                                              &es,
    540                                              GNUNET_TIME_UNIT_FOREVER_REL,
    541                                              &db_notify,
    542                                              NULL);
    543   }
    544   GNUNET_assert (NULL == task);
    545   task = GNUNET_SCHEDULER_add_now (&select_work,
    546                                    NULL);
    547 }
    548 
    549 
    550 /**
    551  * The main function of the taler-merchant-webhook
    552  * @param argc number of arguments from the command line
    553  * @param argv command line arguments
    554  * @return 0 ok, 1 on error
    555  */
    556 int
    557 main (int argc,
    558       char *const *argv)
    559 {
    560   struct GNUNET_GETOPT_CommandLineOption options[] = {
    561     GNUNET_GETOPT_option_flag ('t',
    562                                "test",
    563                                "run in test mode and exit when idle",
    564                                &test_mode),
    565     GNUNET_GETOPT_option_timetravel ('T',
    566                                      "timetravel"),
    567     GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
    568     GNUNET_GETOPT_OPTION_END
    569   };
    570   enum GNUNET_GenericReturnValue ret;
    571 
    572   ret = GNUNET_PROGRAM_run (
    573     TALER_MERCHANT_project_data (),
    574     argc, argv,
    575     "taler-merchant-webhook",
    576     gettext_noop (
    577       "background process that executes webhooks"),
    578     options,
    579     &run, NULL);
    580   if (GNUNET_SYSERR == ret)
    581     return EXIT_INVALIDARGUMENT;
    582   if (GNUNET_NO == ret)
    583     return EXIT_SUCCESS;
    584   return global_ret;
    585 }
    586 
    587 
    588 /* end of taler-merchant-webhook.c */