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 */