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 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 kyclogic_sanctions.c
18 : * @brief wrapper around sanction list evaluator
19 : * @author Christian Grothoff
20 : */
21 : #include "platform.h"
22 : #include "taler/taler_json_lib.h"
23 : #include "taler/taler_kyclogic_lib.h"
24 :
25 :
26 : /**
27 : * Entry in the ordered list of pending evaluations.
28 : */
29 : struct TALER_KYCLOGIC_EvaluationEntry
30 : {
31 : /**
32 : * Kept in a DLL.
33 : */
34 : struct TALER_KYCLOGIC_EvaluationEntry *prev;
35 :
36 : /**
37 : * Kept in a DLL.
38 : */
39 : struct TALER_KYCLOGIC_EvaluationEntry *next;
40 :
41 : /**
42 : * Callback to call with the result.
43 : */
44 : TALER_KYCLOGIC_SanctionResultCallback cb;
45 :
46 : /**
47 : * Closure for @e cb.
48 : */
49 : void *cb_cls;
50 :
51 : /**
52 : * Buffer with data we need to send to the helper.
53 : */
54 : char *write_buf;
55 :
56 : /**
57 : * Total length of @e write_buf.
58 : */
59 : size_t write_size;
60 :
61 : /**
62 : * Current write position in @e write_buf.
63 : */
64 : size_t write_pos;
65 :
66 : };
67 :
68 :
69 : /**
70 : * Handle to a sanction list evaluation helper process.
71 : */
72 : struct TALER_KYCLOGIC_SanctionRater
73 : {
74 :
75 : /**
76 : * Kept in a DLL.
77 : */
78 : struct TALER_KYCLOGIC_EvaluationEntry *ee_head;
79 :
80 : /**
81 : * Kept in a DLL.
82 : */
83 : struct TALER_KYCLOGIC_EvaluationEntry *ee_tail;
84 :
85 : /**
86 : * Handle to the helper process.
87 : */
88 : struct GNUNET_Process *helper;
89 :
90 : /**
91 : * Pipe for the stdin of the @e helper.
92 : */
93 : struct GNUNET_DISK_FileHandle *chld_stdin;
94 :
95 : /**
96 : * Pipe for the stdout of the @e helper.
97 : */
98 : struct GNUNET_DISK_FileHandle *chld_stdout;
99 :
100 : /**
101 : * Handle to wait on the child to terminate.
102 : */
103 : struct GNUNET_ChildWaitHandle *cwh;
104 :
105 : /**
106 : * Task to read JSON output from the child.
107 : */
108 : struct GNUNET_SCHEDULER_Task *read_task;
109 :
110 : /**
111 : * Task to send JSON input to the child.
112 : */
113 : struct GNUNET_SCHEDULER_Task *write_task;
114 :
115 : /**
116 : * Buffer for reading data from the helper.
117 : */
118 : void *read_buf;
119 :
120 : /**
121 : * Current size of @a read_buf.
122 : */
123 : size_t read_size;
124 :
125 : /**
126 : * Current offset in @a read_buf.
127 : */
128 : size_t read_pos;
129 :
130 : };
131 :
132 :
133 : /**
134 : * We encountered a hard error (or explicit stop) of @a sr.
135 : * Shut down processing (but do not yet free @a sr).
136 : *
137 : * @param[in,out] sr sanction rater to fail
138 : */
139 : static void
140 0 : fail_hard (struct TALER_KYCLOGIC_SanctionRater *sr)
141 : {
142 : struct TALER_KYCLOGIC_EvaluationEntry *ee;
143 :
144 0 : if (NULL != sr->chld_stdin)
145 : {
146 0 : GNUNET_break (GNUNET_OK ==
147 : GNUNET_DISK_file_close (sr->chld_stdin));
148 0 : sr->chld_stdin = NULL;
149 : }
150 0 : if (NULL != sr->read_task)
151 : {
152 0 : GNUNET_SCHEDULER_cancel (sr->read_task);
153 0 : sr->read_task = NULL;
154 : }
155 0 : if (NULL != sr->write_task)
156 : {
157 0 : GNUNET_SCHEDULER_cancel (sr->write_task);
158 0 : sr->write_task = NULL;
159 : }
160 0 : if (NULL != sr->helper)
161 : {
162 0 : GNUNET_process_destroy (sr->helper);
163 0 : sr->helper = NULL;
164 : }
165 0 : while (NULL != (ee = sr->ee_tail))
166 : {
167 0 : GNUNET_CONTAINER_DLL_remove (sr->ee_head,
168 : sr->ee_tail,
169 : ee);
170 0 : ee->cb (ee->cb_cls,
171 : TALER_EC_EXCHANGE_GENERIC_KYC_SANCTION_LIST_CHECK_FAILED,
172 : NULL,
173 : 1.0,
174 : 0.0);
175 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
176 : "Failed to send %u bytes to child\n",
177 : (unsigned int) (ee->write_size - ee->write_pos));
178 0 : GNUNET_free (ee->write_buf);
179 0 : GNUNET_free (ee);
180 : }
181 0 : }
182 :
183 :
184 : /**
185 : * Parse data from input buffer.
186 : *
187 : * @param[in,out] sr sanction rater to process data from
188 : * @return true if everything is fine, false on failure
189 : */
190 : static bool
191 0 : process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr)
192 : {
193 0 : char *buf = sr->read_buf;
194 : size_t buf_len;
195 : const void *end;
196 :
197 0 : end = memrchr (sr->read_buf,
198 : '\n',
199 : sr->read_pos);
200 0 : if ( (NULL == end) &&
201 0 : (sr->read_pos < 2048) )
202 0 : return true;
203 0 : if (NULL == end)
204 : {
205 : /* line returned by sanction rater way too long */
206 0 : GNUNET_break (0);
207 0 : return false;
208 : }
209 0 : end++;
210 0 : buf_len = end - sr->read_buf;
211 0 : while (0 != buf_len)
212 : {
213 : char *nl;
214 : double rating;
215 : double confidence;
216 : char best_match[1024];
217 : size_t line_len;
218 :
219 0 : nl = memchr (buf,
220 : '\n',
221 : buf_len);
222 0 : if (NULL == nl)
223 : {
224 : /* no newline in 2048 bytes? not allowed */
225 0 : GNUNET_break (0);
226 0 : return false;
227 : }
228 0 : *nl = '\0';
229 0 : line_len = nl - buf + 1;
230 0 : if (3 !=
231 0 : sscanf (buf,
232 : "%lf %lf %1023s",
233 : &rating,
234 : &confidence,
235 : best_match))
236 : {
237 : /* maybe best_match is empty because literally nothing matched */
238 0 : if (2 !=
239 0 : sscanf (buf,
240 : "%lf %lf ",
241 : &rating,
242 : &confidence))
243 : {
244 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
245 : "Malformed input line `%s'\n",
246 : buf);
247 0 : GNUNET_break (0);
248 0 : return false;
249 : }
250 0 : strcpy (best_match,
251 : "<none>");
252 : }
253 :
254 : {
255 0 : struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail;
256 :
257 : /* This assertion would fail if the helper outputs more lines
258 : than we sent it queries, pointing to some kind of
259 : desynchronization. Failing hard as we cannot really handle
260 : this case nicely. */
261 0 : GNUNET_assert (NULL != ee);
262 0 : GNUNET_CONTAINER_DLL_remove (sr->ee_head,
263 : sr->ee_tail,
264 : ee);
265 0 : ee->cb (ee->cb_cls,
266 : TALER_EC_NONE,
267 : best_match,
268 : rating,
269 : confidence);
270 0 : GNUNET_free (ee->write_buf);
271 0 : GNUNET_free (ee);
272 : }
273 0 : buf += line_len;
274 0 : buf_len -= line_len;
275 : }
276 0 : buf_len = end - sr->read_buf;
277 0 : memmove (sr->read_buf,
278 : end,
279 0 : sr->read_pos - buf_len);
280 0 : sr->read_pos -= buf_len;
281 0 : return true;
282 : }
283 :
284 :
285 : /**
286 : * Function called when we can read more data from
287 : * the child process.
288 : *
289 : * @param cls our `struct TALER_KYCLOGIC_SanctionRater *`
290 : */
291 : static void
292 0 : read_cb (void *cls)
293 : {
294 0 : struct TALER_KYCLOGIC_SanctionRater *sr = cls;
295 :
296 0 : sr->read_task = NULL;
297 : while (1)
298 0 : {
299 : ssize_t ret;
300 :
301 0 : if (sr->read_size == sr->read_pos)
302 : {
303 : /* Grow input buffer */
304 : size_t ns;
305 : void *tmp;
306 :
307 0 : ns = GNUNET_MAX (2 * sr->read_size,
308 : 1024);
309 0 : if (ns > GNUNET_MAX_MALLOC_CHECKED)
310 0 : ns = GNUNET_MAX_MALLOC_CHECKED;
311 0 : if (sr->read_size == ns)
312 : {
313 : /* Helper returned more than 40 MB of data! Stop reading! */
314 0 : GNUNET_break (0);
315 0 : GNUNET_break (GNUNET_OK ==
316 : GNUNET_DISK_file_close (sr->chld_stdin));
317 0 : return;
318 : }
319 0 : tmp = GNUNET_malloc_large (ns);
320 0 : if (NULL == tmp)
321 : {
322 : /* out of memory, also stop reading */
323 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
324 : "malloc");
325 0 : GNUNET_break (GNUNET_OK ==
326 : GNUNET_DISK_file_close (sr->chld_stdin));
327 0 : return;
328 : }
329 0 : GNUNET_memcpy (tmp,
330 : sr->read_buf,
331 : sr->read_pos);
332 0 : GNUNET_free (sr->read_buf);
333 0 : sr->read_buf = tmp;
334 0 : sr->read_size = ns;
335 : }
336 0 : ret = GNUNET_DISK_file_read (sr->chld_stdout,
337 0 : sr->read_buf + sr->read_pos,
338 0 : sr->read_size - sr->read_pos);
339 0 : if (ret < 0)
340 : {
341 0 : if ( (EAGAIN != errno) &&
342 0 : (EWOULDBLOCK != errno) &&
343 0 : (EINTR != errno) )
344 : {
345 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
346 : "read");
347 0 : return;
348 : }
349 : /* Continue later */
350 0 : break;
351 : }
352 0 : if (0 == ret)
353 : {
354 : /* regular end of stream, odd! */
355 0 : fail_hard (sr);
356 0 : return;
357 : }
358 0 : GNUNET_assert (sr->read_size >= sr->read_pos + ret);
359 0 : sr->read_pos += ret;
360 0 : if (! process_buffer (sr))
361 0 : return;
362 : }
363 : sr->read_task
364 0 : = GNUNET_SCHEDULER_add_read_file (
365 0 : GNUNET_TIME_UNIT_FOREVER_REL,
366 0 : sr->chld_stdout,
367 : &read_cb,
368 : sr);
369 : }
370 :
371 :
372 : /**
373 : * Function called when we can write more data to
374 : * the child process.
375 : *
376 : * @param cls our `struct SanctionRater *`
377 : */
378 : static void
379 0 : write_cb (void *cls)
380 : {
381 0 : struct TALER_KYCLOGIC_SanctionRater *sr = cls;
382 0 : struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail;
383 : ssize_t ret;
384 :
385 0 : sr->write_task = NULL;
386 0 : while ( (NULL != ee) &&
387 0 : (ee->write_size == ee->write_pos) )
388 0 : ee = ee->prev;
389 0 : while (NULL != ee)
390 : {
391 0 : while (ee->write_size > ee->write_pos)
392 : {
393 0 : ret = GNUNET_DISK_file_write (sr->chld_stdin,
394 0 : ee->write_buf + ee->write_pos,
395 0 : ee->write_size - ee->write_pos);
396 0 : if (ret < 0)
397 : {
398 0 : if ( (EAGAIN != errno) &&
399 0 : (EINTR != errno) )
400 : {
401 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
402 : "write");
403 : /* helper must have died */
404 0 : fail_hard (sr);
405 0 : return;
406 : }
407 0 : break;
408 : }
409 0 : if (0 == ret)
410 : {
411 0 : GNUNET_break (0);
412 0 : break;
413 : }
414 0 : GNUNET_assert (ee->write_size >= ee->write_pos + ret);
415 0 : ee->write_pos += ret;
416 : }
417 0 : if ( (ee->write_size > ee->write_pos) &&
418 0 : ( (EAGAIN == errno) ||
419 0 : (EWOULDBLOCK == errno) ||
420 0 : (EINTR == errno) ) )
421 : {
422 : sr->write_task
423 0 : = GNUNET_SCHEDULER_add_write_file (
424 0 : GNUNET_TIME_UNIT_FOREVER_REL,
425 0 : sr->chld_stdin,
426 : &write_cb,
427 : sr);
428 0 : return;
429 : }
430 0 : if (ee->write_size == ee->write_pos)
431 : {
432 0 : GNUNET_free (ee->write_buf);
433 0 : ee = ee->prev;
434 : }
435 : } /* while (NULL != ee) */
436 : }
437 :
438 :
439 : /**
440 : * Defines a GNUNET_ChildCompletedCallback which is sent back
441 : * upon death or completion of a child process.
442 : *
443 : * @param cls handle for the callback
444 : * @param type type of the process
445 : * @param exit_code status code of the process
446 : *
447 : */
448 : static void
449 0 : child_done_cb (void *cls,
450 : enum GNUNET_OS_ProcessStatusType type,
451 : long unsigned int exit_code)
452 : {
453 0 : struct TALER_KYCLOGIC_SanctionRater *sr = cls;
454 :
455 0 : sr->cwh = NULL;
456 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
457 : "Conversion helper exited with status %d and code %llu after outputting %llu bytes of data\n",
458 : (int) type,
459 : (unsigned long long) exit_code,
460 : (unsigned long long) sr->read_pos);
461 0 : fail_hard (sr);
462 0 : }
463 :
464 :
465 : struct TALER_KYCLOGIC_SanctionRater *
466 0 : TALER_KYCLOGIC_sanction_rater_start (const char *binary,
467 : char *const*argv)
468 : {
469 : struct TALER_KYCLOGIC_SanctionRater *sr;
470 : struct GNUNET_DISK_PipeHandle *pipe_stdin;
471 : struct GNUNET_DISK_PipeHandle *pipe_stdout;
472 :
473 0 : sr = GNUNET_new (struct TALER_KYCLOGIC_SanctionRater);
474 0 : pipe_stdin = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_READ);
475 0 : GNUNET_assert (NULL != pipe_stdin);
476 0 : pipe_stdout = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_WRITE);
477 0 : GNUNET_assert (NULL != pipe_stdout);
478 0 : sr->helper = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
479 0 : GNUNET_assert (GNUNET_OK ==
480 : GNUNET_process_set_options (
481 : sr->helper,
482 : GNUNET_process_option_inherit_rpipe (pipe_stdin,
483 : STDIN_FILENO),
484 : GNUNET_process_option_inherit_wpipe (pipe_stdout,
485 : STDOUT_FILENO)));
486 0 : if (GNUNET_OK !=
487 0 : GNUNET_process_run_command_argv (sr->helper,
488 : binary,
489 : (const char **) argv))
490 : {
491 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
492 : "Failed to run conversion helper `%s'\n",
493 : binary);
494 0 : GNUNET_process_destroy (sr->helper);
495 0 : GNUNET_break (GNUNET_OK ==
496 : GNUNET_DISK_pipe_close (pipe_stdin));
497 0 : GNUNET_break (GNUNET_OK ==
498 : GNUNET_DISK_pipe_close (pipe_stdout));
499 0 : GNUNET_free (sr);
500 0 : return NULL;
501 : }
502 0 : sr->chld_stdin =
503 0 : GNUNET_DISK_pipe_detach_end (pipe_stdin,
504 : GNUNET_DISK_PIPE_END_WRITE);
505 0 : sr->chld_stdout =
506 0 : GNUNET_DISK_pipe_detach_end (pipe_stdout,
507 : GNUNET_DISK_PIPE_END_READ);
508 0 : GNUNET_break (GNUNET_OK ==
509 : GNUNET_DISK_pipe_close (pipe_stdin));
510 0 : GNUNET_break (GNUNET_OK ==
511 : GNUNET_DISK_pipe_close (pipe_stdout));
512 :
513 : sr->read_task
514 0 : = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
515 0 : sr->chld_stdout,
516 : &read_cb,
517 : sr);
518 0 : sr->cwh = GNUNET_wait_child (sr->helper,
519 : &child_done_cb,
520 : sr);
521 0 : return sr;
522 : }
523 :
524 :
525 : struct TALER_KYCLOGIC_EvaluationEntry *
526 0 : TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr,
527 : const json_t *attributes,
528 : TALER_KYCLOGIC_SanctionResultCallback cb,
529 : void *cb_cls)
530 : {
531 : struct TALER_KYCLOGIC_EvaluationEntry *ee;
532 : char *js;
533 :
534 0 : if (NULL == sr->read_task)
535 : {
536 0 : GNUNET_break (0);
537 0 : return NULL;
538 : }
539 0 : ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry);
540 0 : ee->cb = cb;
541 0 : ee->cb_cls = cb_cls;
542 0 : GNUNET_CONTAINER_DLL_insert (sr->ee_head,
543 : sr->ee_tail,
544 : ee);
545 0 : js = json_dumps (attributes,
546 : JSON_COMPACT);
547 0 : GNUNET_asprintf (&ee->write_buf,
548 : "%s\n",
549 : js);
550 0 : free (js);
551 0 : ee->write_size = strlen (ee->write_buf);
552 0 : if (NULL == sr->write_task)
553 : sr->write_task
554 0 : = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL,
555 0 : sr->chld_stdin,
556 : &write_cb,
557 : sr);
558 0 : return ee;
559 : }
560 :
561 :
562 : void
563 0 : TALER_KYCLOGIC_sanction_rater_stop (
564 : struct TALER_KYCLOGIC_SanctionRater *sr)
565 : {
566 0 : fail_hard (sr);
567 0 : if (NULL != sr->cwh)
568 : {
569 0 : GNUNET_wait_child_cancel (sr->cwh);
570 0 : sr->cwh = NULL;
571 : }
572 0 : if (NULL != sr->write_task)
573 : {
574 0 : GNUNET_SCHEDULER_cancel (sr->write_task);
575 0 : sr->write_task = NULL;
576 : }
577 0 : if (NULL != sr->chld_stdout)
578 : {
579 0 : GNUNET_break (GNUNET_OK ==
580 : GNUNET_DISK_file_close (sr->chld_stdout));
581 0 : sr->chld_stdout = NULL;
582 : }
583 0 : GNUNET_free (sr->read_buf);
584 0 : GNUNET_free (sr);
585 0 : }
|