kyclogic_sanctions.c (14912B)
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 fail_hard (struct TALER_KYCLOGIC_SanctionRater *sr) 141 { 142 struct TALER_KYCLOGIC_EvaluationEntry *ee; 143 144 if (NULL != sr->chld_stdin) 145 { 146 GNUNET_break (GNUNET_OK == 147 GNUNET_DISK_file_close (sr->chld_stdin)); 148 sr->chld_stdin = NULL; 149 } 150 if (NULL != sr->read_task) 151 { 152 GNUNET_SCHEDULER_cancel (sr->read_task); 153 sr->read_task = NULL; 154 } 155 if (NULL != sr->write_task) 156 { 157 GNUNET_SCHEDULER_cancel (sr->write_task); 158 sr->write_task = NULL; 159 } 160 if (NULL != sr->helper) 161 { 162 GNUNET_process_destroy (sr->helper); 163 sr->helper = NULL; 164 } 165 while (NULL != (ee = sr->ee_tail)) 166 { 167 GNUNET_CONTAINER_DLL_remove (sr->ee_head, 168 sr->ee_tail, 169 ee); 170 ee->cb (ee->cb_cls, 171 TALER_EC_EXCHANGE_GENERIC_KYC_SANCTION_LIST_CHECK_FAILED, 172 NULL, 173 1.0, 174 0.0); 175 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 GNUNET_free (ee->write_buf); 179 GNUNET_free (ee); 180 } 181 } 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 process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr) 192 { 193 char *buf = sr->read_buf; 194 size_t buf_len; 195 const void *end; 196 197 end = memrchr (sr->read_buf, 198 '\n', 199 sr->read_pos); 200 if ( (NULL == end) && 201 (sr->read_pos < 2048) ) 202 return true; 203 if (NULL == end) 204 { 205 /* line returned by sanction rater way too long */ 206 GNUNET_break (0); 207 return false; 208 } 209 end++; 210 buf_len = end - sr->read_buf; 211 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 nl = memchr (buf, 220 '\n', 221 buf_len); 222 if (NULL == nl) 223 { 224 /* no newline in 2048 bytes? not allowed */ 225 GNUNET_break (0); 226 return false; 227 } 228 *nl = '\0'; 229 line_len = nl - buf + 1; 230 if (3 != 231 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 if (2 != 239 sscanf (buf, 240 "%lf %lf ", 241 &rating, 242 &confidence)) 243 { 244 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 245 "Malformed input line `%s'\n", 246 buf); 247 GNUNET_break (0); 248 return false; 249 } 250 strcpy (best_match, 251 "<none>"); 252 } 253 254 { 255 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 GNUNET_assert (NULL != ee); 262 GNUNET_CONTAINER_DLL_remove (sr->ee_head, 263 sr->ee_tail, 264 ee); 265 ee->cb (ee->cb_cls, 266 TALER_EC_NONE, 267 best_match, 268 rating, 269 confidence); 270 GNUNET_free (ee->write_buf); 271 GNUNET_free (ee); 272 } 273 buf += line_len; 274 buf_len -= line_len; 275 } 276 buf_len = end - sr->read_buf; 277 memmove (sr->read_buf, 278 end, 279 sr->read_pos - buf_len); 280 sr->read_pos -= buf_len; 281 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 read_cb (void *cls) 293 { 294 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 295 296 sr->read_task = NULL; 297 while (1) 298 { 299 ssize_t ret; 300 301 if (sr->read_size == sr->read_pos) 302 { 303 /* Grow input buffer */ 304 size_t ns; 305 void *tmp; 306 307 ns = GNUNET_MAX (2 * sr->read_size, 308 1024); 309 if (ns > GNUNET_MAX_MALLOC_CHECKED) 310 ns = GNUNET_MAX_MALLOC_CHECKED; 311 if (sr->read_size == ns) 312 { 313 /* Helper returned more than 40 MB of data! Stop reading! */ 314 GNUNET_break (0); 315 fail_hard (sr); 316 return; 317 } 318 tmp = GNUNET_malloc_large (ns); 319 if (NULL == tmp) 320 { 321 /* out of memory, also stop reading */ 322 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 323 "malloc"); 324 fail_hard (sr); 325 return; 326 } 327 GNUNET_memcpy (tmp, 328 sr->read_buf, 329 sr->read_pos); 330 GNUNET_free (sr->read_buf); 331 sr->read_buf = tmp; 332 sr->read_size = ns; 333 } 334 ret = GNUNET_DISK_file_read (sr->chld_stdout, 335 sr->read_buf + sr->read_pos, 336 sr->read_size - sr->read_pos); 337 if (ret < 0) 338 { 339 if ( (EAGAIN != errno) && 340 (EWOULDBLOCK != errno) && 341 (EINTR != errno) ) 342 { 343 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 344 "read"); 345 fail_hard (sr); 346 return; 347 } 348 /* Continue later */ 349 break; 350 } 351 if (0 == ret) 352 { 353 /* regular end of stream, odd! */ 354 fail_hard (sr); 355 return; 356 } 357 GNUNET_assert (sr->read_size >= sr->read_pos + ret); 358 sr->read_pos += ret; 359 if (! process_buffer (sr)) 360 { 361 fail_hard (sr); 362 return; 363 } 364 } 365 sr->read_task 366 = GNUNET_SCHEDULER_add_read_file ( 367 GNUNET_TIME_UNIT_FOREVER_REL, 368 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 write_cb (void *cls) 382 { 383 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 384 struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail; 385 ssize_t ret; 386 387 sr->write_task = NULL; 388 while ( (NULL != ee) && 389 (ee->write_size == ee->write_pos) ) 390 ee = ee->prev; 391 while (NULL != ee) 392 { 393 bool retry = false; 394 395 while (ee->write_size > ee->write_pos) 396 { 397 ret = GNUNET_DISK_file_write (sr->chld_stdin, 398 ee->write_buf + ee->write_pos, 399 ee->write_size - ee->write_pos); 400 if (ret < 0) 401 { 402 if ( (EAGAIN != errno) && 403 (EWOULDBLOCK != errno) && 404 (EINTR != errno) ) 405 { 406 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 407 "write"); 408 /* helper must have died */ 409 fail_hard (sr); 410 return; 411 } 412 /* continue later */ 413 retry = true; 414 break; 415 } 416 if (0 == ret) 417 { 418 /* should be impossible on a pipe */ 419 GNUNET_break (0); 420 fail_hard (sr); 421 return; 422 } 423 GNUNET_assert (ee->write_size >= ee->write_pos + ret); 424 ee->write_pos += ret; 425 } 426 if (retry) 427 { 428 sr->write_task 429 = GNUNET_SCHEDULER_add_write_file ( 430 GNUNET_TIME_UNIT_FOREVER_REL, 431 sr->chld_stdin, 432 &write_cb, 433 sr); 434 return; 435 } 436 GNUNET_free (ee->write_buf); 437 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 child_done_cb (void *cls, 453 enum GNUNET_OS_ProcessStatusType type, 454 long unsigned int exit_code) 455 { 456 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 457 458 sr->cwh = NULL; 459 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 fail_hard (sr); 465 } 466 467 468 struct TALER_KYCLOGIC_SanctionRater * 469 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 sr = GNUNET_new (struct TALER_KYCLOGIC_SanctionRater); 477 pipe_stdin = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_READ); 478 GNUNET_assert (NULL != pipe_stdin); 479 pipe_stdout = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_WRITE); 480 GNUNET_assert (NULL != pipe_stdout); 481 sr->helper = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 482 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 if (GNUNET_OK != 490 GNUNET_process_run_command_argv (sr->helper, 491 binary, 492 (const char **) argv)) 493 { 494 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 495 "Failed to run conversion helper `%s'\n", 496 binary); 497 GNUNET_process_destroy (sr->helper); 498 GNUNET_break (GNUNET_OK == 499 GNUNET_DISK_pipe_close (pipe_stdin)); 500 GNUNET_break (GNUNET_OK == 501 GNUNET_DISK_pipe_close (pipe_stdout)); 502 GNUNET_free (sr); 503 return NULL; 504 } 505 sr->chld_stdin = 506 GNUNET_DISK_pipe_detach_end (pipe_stdin, 507 GNUNET_DISK_PIPE_END_WRITE); 508 sr->chld_stdout = 509 GNUNET_DISK_pipe_detach_end (pipe_stdout, 510 GNUNET_DISK_PIPE_END_READ); 511 GNUNET_break (GNUNET_OK == 512 GNUNET_DISK_pipe_close (pipe_stdin)); 513 GNUNET_break (GNUNET_OK == 514 GNUNET_DISK_pipe_close (pipe_stdout)); 515 516 sr->read_task 517 = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, 518 sr->chld_stdout, 519 &read_cb, 520 sr); 521 sr->cwh = GNUNET_wait_child (sr->helper, 522 &child_done_cb, 523 sr); 524 return sr; 525 } 526 527 528 struct TALER_KYCLOGIC_EvaluationEntry * 529 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 if (NULL == sr->read_task) 538 { 539 GNUNET_break (0); 540 return NULL; 541 } 542 ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry); 543 ee->cb = cb; 544 ee->cb_cls = cb_cls; 545 GNUNET_CONTAINER_DLL_insert (sr->ee_head, 546 sr->ee_tail, 547 ee); 548 js = json_dumps (attributes, 549 JSON_COMPACT); 550 GNUNET_asprintf (&ee->write_buf, 551 "%s\n", 552 js); 553 free (js); 554 ee->write_size = strlen (ee->write_buf); 555 if (NULL == sr->write_task) 556 sr->write_task 557 = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL, 558 sr->chld_stdin, 559 &write_cb, 560 sr); 561 return ee; 562 } 563 564 565 void 566 TALER_KYCLOGIC_sanction_rater_stop ( 567 struct TALER_KYCLOGIC_SanctionRater *sr) 568 { 569 fail_hard (sr); 570 if (NULL != sr->cwh) 571 { 572 GNUNET_wait_child_cancel (sr->cwh); 573 sr->cwh = NULL; 574 } 575 if (NULL != sr->write_task) 576 { 577 GNUNET_SCHEDULER_cancel (sr->write_task); 578 sr->write_task = NULL; 579 } 580 if (NULL != sr->chld_stdout) 581 { 582 GNUNET_break (GNUNET_OK == 583 GNUNET_DISK_file_close (sr->chld_stdout)); 584 sr->chld_stdout = NULL; 585 } 586 GNUNET_free (sr->read_buf); 587 GNUNET_free (sr); 588 }