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 : fail_hard (sr);
316 0 : return;
317 : }
318 0 : tmp = GNUNET_malloc_large (ns);
319 0 : if (NULL == tmp)
320 : {
321 : /* out of memory, also stop reading */
322 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
323 : "malloc");
324 0 : fail_hard (sr);
325 0 : return;
326 : }
327 0 : GNUNET_memcpy (tmp,
328 : sr->read_buf,
329 : sr->read_pos);
330 0 : GNUNET_free (sr->read_buf);
331 0 : sr->read_buf = tmp;
332 0 : sr->read_size = ns;
333 : }
334 0 : ret = GNUNET_DISK_file_read (sr->chld_stdout,
335 0 : sr->read_buf + sr->read_pos,
336 0 : sr->read_size - sr->read_pos);
337 0 : if (ret < 0)
338 : {
339 0 : if ( (EAGAIN != errno) &&
340 0 : (EWOULDBLOCK != errno) &&
341 0 : (EINTR != errno) )
342 : {
343 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
344 : "read");
345 0 : fail_hard (sr);
346 0 : return;
347 : }
348 : /* Continue later */
349 0 : break;
350 : }
351 0 : if (0 == ret)
352 : {
353 : /* regular end of stream, odd! */
354 0 : fail_hard (sr);
355 0 : return;
356 : }
357 0 : GNUNET_assert (sr->read_size >= sr->read_pos + ret);
358 0 : sr->read_pos += ret;
359 0 : if (! process_buffer (sr))
360 : {
361 0 : fail_hard (sr);
362 0 : return;
363 : }
364 : }
365 : sr->read_task
366 0 : = GNUNET_SCHEDULER_add_read_file (
367 0 : GNUNET_TIME_UNIT_FOREVER_REL,
368 0 : sr->chld_stdout,
369 : &read_cb,
370 : sr);
371 : }
372 :
373 :
374 : /**
375 : * Function called when we can write more data to
376 : * the child process.
377 : *
378 : * @param cls our `struct SanctionRater *`
379 : */
380 : static void
381 0 : write_cb (void *cls)
382 : {
383 0 : struct TALER_KYCLOGIC_SanctionRater *sr = cls;
384 0 : struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail;
385 : ssize_t ret;
386 :
387 0 : sr->write_task = NULL;
388 0 : while ( (NULL != ee) &&
389 0 : (ee->write_size == ee->write_pos) )
390 0 : ee = ee->prev;
391 0 : while (NULL != ee)
392 : {
393 0 : bool retry = false;
394 :
395 0 : while (ee->write_size > ee->write_pos)
396 : {
397 0 : ret = GNUNET_DISK_file_write (sr->chld_stdin,
398 0 : ee->write_buf + ee->write_pos,
399 0 : ee->write_size - ee->write_pos);
400 0 : if (ret < 0)
401 : {
402 0 : if ( (EAGAIN != errno) &&
403 0 : (EWOULDBLOCK != errno) &&
404 0 : (EINTR != errno) )
405 : {
406 0 : GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING,
407 : "write");
408 : /* helper must have died */
409 0 : fail_hard (sr);
410 0 : return;
411 : }
412 : /* continue later */
413 0 : retry = true;
414 0 : break;
415 : }
416 0 : if (0 == ret)
417 : {
418 : /* should be impossible on a pipe */
419 0 : GNUNET_break (0);
420 0 : fail_hard (sr);
421 0 : return;
422 : }
423 0 : GNUNET_assert (ee->write_size >= ee->write_pos + ret);
424 0 : ee->write_pos += ret;
425 : }
426 0 : if (retry)
427 : {
428 : sr->write_task
429 0 : = GNUNET_SCHEDULER_add_write_file (
430 0 : GNUNET_TIME_UNIT_FOREVER_REL,
431 0 : sr->chld_stdin,
432 : &write_cb,
433 : sr);
434 0 : return;
435 : }
436 0 : GNUNET_free (ee->write_buf);
437 0 : ee = ee->prev;
438 : } /* while (NULL != ee) */
439 : }
440 :
441 :
442 : /**
443 : * Defines a GNUNET_ChildCompletedCallback which is sent back
444 : * upon death or completion of a child process.
445 : *
446 : * @param cls handle for the callback
447 : * @param type type of the process
448 : * @param exit_code status code of the process
449 : *
450 : */
451 : static void
452 0 : child_done_cb (void *cls,
453 : enum GNUNET_OS_ProcessStatusType type,
454 : long unsigned int exit_code)
455 : {
456 0 : struct TALER_KYCLOGIC_SanctionRater *sr = cls;
457 :
458 0 : sr->cwh = NULL;
459 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
460 : "Conversion helper exited with status %d and code %llu after outputting %llu bytes of data\n",
461 : (int) type,
462 : (unsigned long long) exit_code,
463 : (unsigned long long) sr->read_pos);
464 0 : fail_hard (sr);
465 0 : }
466 :
467 :
468 : struct TALER_KYCLOGIC_SanctionRater *
469 0 : TALER_KYCLOGIC_sanction_rater_start (const char *binary,
470 : char *const*argv)
471 : {
472 : struct TALER_KYCLOGIC_SanctionRater *sr;
473 : struct GNUNET_DISK_PipeHandle *pipe_stdin;
474 : struct GNUNET_DISK_PipeHandle *pipe_stdout;
475 :
476 0 : sr = GNUNET_new (struct TALER_KYCLOGIC_SanctionRater);
477 0 : pipe_stdin = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_READ);
478 0 : GNUNET_assert (NULL != pipe_stdin);
479 0 : pipe_stdout = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_WRITE);
480 0 : GNUNET_assert (NULL != pipe_stdout);
481 0 : sr->helper = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
482 0 : GNUNET_assert (GNUNET_OK ==
483 : GNUNET_process_set_options (
484 : sr->helper,
485 : GNUNET_process_option_inherit_rpipe (pipe_stdin,
486 : STDIN_FILENO),
487 : GNUNET_process_option_inherit_wpipe (pipe_stdout,
488 : STDOUT_FILENO)));
489 0 : if (GNUNET_OK !=
490 0 : GNUNET_process_run_command_argv (sr->helper,
491 : binary,
492 : (const char **) argv))
493 : {
494 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
495 : "Failed to run conversion helper `%s'\n",
496 : binary);
497 0 : GNUNET_process_destroy (sr->helper);
498 0 : GNUNET_break (GNUNET_OK ==
499 : GNUNET_DISK_pipe_close (pipe_stdin));
500 0 : GNUNET_break (GNUNET_OK ==
501 : GNUNET_DISK_pipe_close (pipe_stdout));
502 0 : GNUNET_free (sr);
503 0 : return NULL;
504 : }
505 0 : sr->chld_stdin =
506 0 : GNUNET_DISK_pipe_detach_end (pipe_stdin,
507 : GNUNET_DISK_PIPE_END_WRITE);
508 0 : sr->chld_stdout =
509 0 : GNUNET_DISK_pipe_detach_end (pipe_stdout,
510 : GNUNET_DISK_PIPE_END_READ);
511 0 : GNUNET_break (GNUNET_OK ==
512 : GNUNET_DISK_pipe_close (pipe_stdin));
513 0 : GNUNET_break (GNUNET_OK ==
514 : GNUNET_DISK_pipe_close (pipe_stdout));
515 :
516 : sr->read_task
517 0 : = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
518 0 : sr->chld_stdout,
519 : &read_cb,
520 : sr);
521 0 : sr->cwh = GNUNET_wait_child (sr->helper,
522 : &child_done_cb,
523 : sr);
524 0 : return sr;
525 : }
526 :
527 :
528 : struct TALER_KYCLOGIC_EvaluationEntry *
529 0 : TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr,
530 : const json_t *attributes,
531 : TALER_KYCLOGIC_SanctionResultCallback cb,
532 : void *cb_cls)
533 : {
534 : struct TALER_KYCLOGIC_EvaluationEntry *ee;
535 : char *js;
536 :
537 0 : if (NULL == sr->read_task)
538 : {
539 0 : GNUNET_break (0);
540 0 : return NULL;
541 : }
542 0 : ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry);
543 0 : ee->cb = cb;
544 0 : ee->cb_cls = cb_cls;
545 0 : GNUNET_CONTAINER_DLL_insert (sr->ee_head,
546 : sr->ee_tail,
547 : ee);
548 0 : js = json_dumps (attributes,
549 : JSON_COMPACT);
550 0 : GNUNET_asprintf (&ee->write_buf,
551 : "%s\n",
552 : js);
553 0 : free (js);
554 0 : ee->write_size = strlen (ee->write_buf);
555 0 : if (NULL == sr->write_task)
556 : sr->write_task
557 0 : = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL,
558 0 : sr->chld_stdin,
559 : &write_cb,
560 : sr);
561 0 : return ee;
562 : }
563 :
564 :
565 : void
566 0 : TALER_KYCLOGIC_sanction_rater_stop (
567 : struct TALER_KYCLOGIC_SanctionRater *sr)
568 : {
569 0 : fail_hard (sr);
570 0 : if (NULL != sr->cwh)
571 : {
572 0 : GNUNET_wait_child_cancel (sr->cwh);
573 0 : sr->cwh = NULL;
574 : }
575 0 : if (NULL != sr->write_task)
576 : {
577 0 : GNUNET_SCHEDULER_cancel (sr->write_task);
578 0 : sr->write_task = NULL;
579 : }
580 0 : if (NULL != sr->chld_stdout)
581 : {
582 0 : GNUNET_break (GNUNET_OK ==
583 : GNUNET_DISK_file_close (sr->chld_stdout));
584 0 : sr->chld_stdout = NULL;
585 : }
586 0 : GNUNET_free (sr->read_buf);
587 0 : GNUNET_free (sr);
588 0 : }
|