Line data Source code
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 15 : shutdown_task (void *cls)
92 : {
93 : struct WorkResponse *w;
94 :
95 : (void) cls;
96 15 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
97 : "Running shutdown\n");
98 15 : if (NULL != event_handler)
99 : {
100 15 : db_plugin->event_listen_cancel (event_handler);
101 15 : event_handler = NULL;
102 : }
103 15 : if (NULL != task)
104 : {
105 13 : GNUNET_SCHEDULER_cancel (task);
106 13 : task = NULL;
107 : }
108 15 : while (NULL != (w = w_head))
109 : {
110 0 : GNUNET_CONTAINER_DLL_remove (w_head,
111 : w_tail,
112 : w);
113 0 : GNUNET_CURL_job_cancel (w->job);
114 0 : curl_slist_free_all (w->job_headers);
115 0 : GNUNET_free (w->body);
116 0 : GNUNET_free (w);
117 : }
118 15 : db_plugin->rollback (db_plugin->cls); /* just in case */
119 15 : TALER_MERCHANTDB_plugin_unload (db_plugin);
120 15 : db_plugin = NULL;
121 15 : cfg = NULL;
122 15 : if (NULL != ctx)
123 : {
124 15 : GNUNET_CURL_fini (ctx);
125 15 : ctx = NULL;
126 : }
127 15 : if (NULL != rc)
128 : {
129 15 : GNUNET_CURL_gnunet_rc_destroy (rc);
130 15 : rc = NULL;
131 : }
132 15 : }
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 2 : handle_webhook_response (void *cls,
155 : long response_code,
156 : const void *body,
157 : size_t body_size)
158 : {
159 2 : struct WorkResponse *w = cls;
160 :
161 : (void) body;
162 : (void) body_size;
163 2 : w->job = NULL;
164 2 : GNUNET_CONTAINER_DLL_remove (w_head,
165 : w_tail,
166 : w);
167 2 : GNUNET_free (w->body);
168 2 : curl_slist_free_all (w->job_headers);
169 2 : if (NULL == w_head)
170 2 : task = GNUNET_SCHEDULER_add_now (&select_work,
171 : NULL);
172 2 : 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 2 : if (2 == response_code / 100) /* any 2xx http status code is OK! */
177 : {
178 : enum GNUNET_DB_QueryStatus qs;
179 :
180 2 : qs = db_plugin->delete_pending_webhook (db_plugin->cls,
181 : w->webhook_pending_serial);
182 2 : GNUNET_free (w);
183 2 : switch (qs)
184 : {
185 0 : case GNUNET_DB_STATUS_HARD_ERROR:
186 : case GNUNET_DB_STATUS_SOFT_ERROR:
187 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
188 : "Failed to delete webhook, delete returned: %d\n",
189 : qs);
190 0 : global_ret = EXIT_FAILURE;
191 0 : GNUNET_SCHEDULER_shutdown ();
192 0 : return;
193 2 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
194 2 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
195 : "Delete returned: %d\n",
196 : qs);
197 2 : return;
198 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
199 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
200 : "Delete returned: %d\n",
201 : qs);
202 0 : return;
203 : }
204 0 : GNUNET_assert (0);
205 : }
206 :
207 : {
208 : struct GNUNET_TIME_Relative next_attempt;
209 : enum GNUNET_DB_QueryStatus qs;
210 0 : switch (response_code)
211 : {
212 0 : case MHD_HTTP_BAD_REQUEST:
213 0 : next_attempt = GNUNET_TIME_UNIT_FOREVER_REL; // never try again
214 0 : break;
215 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
216 0 : next_attempt = GNUNET_TIME_UNIT_MINUTES;
217 0 : break;
218 0 : case MHD_HTTP_FORBIDDEN:
219 0 : next_attempt = GNUNET_TIME_UNIT_MINUTES;
220 0 : break;
221 0 : default:
222 0 : next_attempt = GNUNET_TIME_UNIT_HOURS;
223 0 : break;
224 : }
225 0 : qs = db_plugin->update_pending_webhook (db_plugin->cls,
226 : w->webhook_pending_serial,
227 : GNUNET_TIME_relative_to_absolute (
228 : next_attempt));
229 0 : GNUNET_free (w);
230 0 : switch (qs)
231 : {
232 0 : case GNUNET_DB_STATUS_HARD_ERROR:
233 : case GNUNET_DB_STATUS_SOFT_ERROR:
234 0 : 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 0 : global_ret = EXIT_FAILURE;
240 0 : GNUNET_SCHEDULER_shutdown ();
241 0 : return;
242 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
243 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
244 : "Next in %s Rval: %d\n",
245 : GNUNET_TIME_relative2s (next_attempt, true),
246 : qs);
247 0 : return;
248 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
249 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
250 : "Next in %s Rval: %d\n",
251 : GNUNET_TIME_relative2s (next_attempt, true),
252 : qs);
253 0 : return;
254 : }
255 0 : 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 2 : 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 2 : struct WorkResponse *w = GNUNET_new (struct WorkResponse);
283 : CURL *eh;
284 2 : struct curl_slist *job_headers = NULL;
285 :
286 : (void) retries;
287 : (void) next_attempt;
288 : (void) cls;
289 2 : GNUNET_CONTAINER_DLL_insert (w_head,
290 : w_tail,
291 : w);
292 2 : w->webhook_pending_serial = webhook_pending_serial;
293 2 : eh = curl_easy_init ();
294 2 : GNUNET_assert (NULL != eh);
295 2 : GNUNET_assert (CURLE_OK ==
296 : curl_easy_setopt (eh,
297 : CURLOPT_CUSTOMREQUEST,
298 : http_method));
299 2 : GNUNET_assert (CURLE_OK ==
300 : curl_easy_setopt (eh,
301 : CURLOPT_URL,
302 : url));
303 2 : GNUNET_assert (CURLE_OK ==
304 : curl_easy_setopt (eh,
305 : CURLOPT_VERBOSE,
306 : 0L));
307 :
308 : /* conversion body data */
309 2 : if (NULL != body)
310 : {
311 2 : w->body = GNUNET_strdup (body);
312 2 : GNUNET_assert (CURLE_OK ==
313 : curl_easy_setopt (eh,
314 : CURLOPT_POSTFIELDS,
315 : w->body));
316 : }
317 : /* conversion header to job_headers data */
318 2 : if (NULL != header)
319 : {
320 2 : char *header_copy = GNUNET_strdup (header);
321 :
322 2 : for (const char *tok = strtok (header_copy, "\n");
323 4 : NULL != tok;
324 2 : tok = strtok (NULL, "\n"))
325 : {
326 : // extract all Key: value from 'header_copy'!
327 2 : job_headers = curl_slist_append (job_headers,
328 : tok);
329 : }
330 2 : GNUNET_free (header_copy);
331 2 : GNUNET_assert (CURLE_OK ==
332 : curl_easy_setopt (eh,
333 : CURLOPT_HTTPHEADER,
334 : job_headers));
335 2 : w->job_headers = job_headers;
336 : }
337 2 : GNUNET_assert (CURLE_OK ==
338 : curl_easy_setopt (eh,
339 : CURLOPT_MAXREDIRS,
340 : 5));
341 2 : GNUNET_assert (CURLE_OK ==
342 : curl_easy_setopt (eh,
343 : CURLOPT_FOLLOWLOCATION,
344 : 1));
345 :
346 2 : w->job = GNUNET_CURL_job_add_raw (ctx,
347 : eh,
348 : job_headers,
349 : &handle_webhook_response,
350 : w);
351 2 : if (NULL == w->job)
352 : {
353 0 : 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 0 : curl_slist_free_all (w->job_headers);
357 0 : GNUNET_free (w->body);
358 0 : GNUNET_CONTAINER_DLL_remove (w_head,
359 : w_tail,
360 : w);
361 0 : GNUNET_free (w);
362 0 : GNUNET_SCHEDULER_shutdown ();
363 0 : 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 24 : 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 24 : GNUNET_assert (NULL != task);
385 24 : GNUNET_SCHEDULER_cancel (task);
386 24 : task = GNUNET_SCHEDULER_add_now (&select_work,
387 : NULL);
388 24 : }
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 0 : 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 0 : task = GNUNET_SCHEDULER_add_at (next_attempt,
421 : &select_work,
422 : NULL);
423 0 : }
424 :
425 :
426 : static void
427 41 : select_work (void *cls)
428 : {
429 : enum GNUNET_DB_QueryStatus qs;
430 : struct GNUNET_TIME_Relative rel;
431 :
432 : (void) cls;
433 41 : task = NULL;
434 41 : db_plugin->preflight (db_plugin->cls);
435 41 : qs = db_plugin->lookup_pending_webhooks (db_plugin->cls,
436 : &pending_webhooks_cb,
437 : NULL);
438 41 : switch (qs)
439 : {
440 0 : case GNUNET_DB_STATUS_HARD_ERROR:
441 : case GNUNET_DB_STATUS_SOFT_ERROR:
442 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
443 : "Failed to lookup pending webhooks!\n");
444 0 : global_ret = EXIT_FAILURE;
445 0 : GNUNET_SCHEDULER_shutdown ();
446 0 : return;
447 39 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
448 39 : if (test_mode)
449 : {
450 2 : GNUNET_SCHEDULER_shutdown ();
451 2 : return;
452 : }
453 37 : qs = db_plugin->lookup_future_webhook (db_plugin->cls,
454 : &future_webhook_cb,
455 : NULL);
456 : switch (qs)
457 : {
458 0 : case GNUNET_DB_STATUS_HARD_ERROR:
459 : case GNUNET_DB_STATUS_SOFT_ERROR:
460 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
461 : "Failed to lookup future webhook!\n");
462 0 : global_ret = EXIT_FAILURE;
463 0 : GNUNET_SCHEDULER_shutdown ();
464 0 : return;
465 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
466 0 : return;
467 37 : 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 37 : rel = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 5);
472 37 : task = GNUNET_SCHEDULER_add_delayed (rel,
473 : &select_work,
474 : NULL);
475 37 : return;
476 : }
477 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
478 : default:
479 2 : 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 15 : 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 15 : cfg = c;
502 15 : GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
503 : NULL);
504 15 : ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
505 : &rc);
506 15 : rc = GNUNET_CURL_gnunet_rc_create (ctx);
507 15 : if (NULL == ctx)
508 : {
509 0 : GNUNET_break (0);
510 0 : GNUNET_SCHEDULER_shutdown ();
511 0 : global_ret = EXIT_FAILURE;
512 0 : return;
513 : }
514 15 : if (NULL ==
515 15 : (db_plugin = TALER_MERCHANTDB_plugin_load (cfg)))
516 : {
517 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
518 : "Failed to initialize DB subsystem\n");
519 0 : GNUNET_SCHEDULER_shutdown ();
520 0 : global_ret = EXIT_NOTCONFIGURED;
521 0 : return;
522 : }
523 15 : if (GNUNET_OK !=
524 15 : db_plugin->connect (db_plugin->cls))
525 : {
526 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
527 : "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
528 0 : GNUNET_SCHEDULER_shutdown ();
529 0 : global_ret = EXIT_FAILURE;
530 0 : return;
531 : }
532 : {
533 15 : struct GNUNET_DB_EventHeaderP es = {
534 15 : .size = htons (sizeof (es)),
535 15 : .type = htons (TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING)
536 : };
537 :
538 30 : event_handler = db_plugin->event_listen (db_plugin->cls,
539 : &es,
540 15 : GNUNET_TIME_UNIT_FOREVER_REL,
541 : &db_notify,
542 : NULL);
543 : }
544 15 : GNUNET_assert (NULL == task);
545 15 : 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 15 : main (int argc,
558 : char *const *argv)
559 : {
560 15 : struct GNUNET_GETOPT_CommandLineOption options[] = {
561 15 : GNUNET_GETOPT_option_flag ('t',
562 : "test",
563 : "run in test mode and exit when idle",
564 : &test_mode),
565 15 : GNUNET_GETOPT_option_timetravel ('T',
566 : "timetravel"),
567 15 : GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
568 : GNUNET_GETOPT_OPTION_END
569 : };
570 : enum GNUNET_GenericReturnValue ret;
571 :
572 15 : 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 15 : if (GNUNET_SYSERR == ret)
581 0 : return EXIT_INVALIDARGUMENT;
582 15 : if (GNUNET_NO == ret)
583 0 : return EXIT_SUCCESS;
584 15 : return global_ret;
585 : }
586 :
587 :
588 : /* end of taler-merchant-webhook.c */
|