kyclogic_sanctions.c (14977B)
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 GNUNET_break (GNUNET_OK == 316 GNUNET_DISK_file_close (sr->chld_stdin)); 317 return; 318 } 319 tmp = GNUNET_malloc_large (ns); 320 if (NULL == tmp) 321 { 322 /* out of memory, also stop reading */ 323 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 324 "malloc"); 325 GNUNET_break (GNUNET_OK == 326 GNUNET_DISK_file_close (sr->chld_stdin)); 327 return; 328 } 329 GNUNET_memcpy (tmp, 330 sr->read_buf, 331 sr->read_pos); 332 GNUNET_free (sr->read_buf); 333 sr->read_buf = tmp; 334 sr->read_size = ns; 335 } 336 ret = GNUNET_DISK_file_read (sr->chld_stdout, 337 sr->read_buf + sr->read_pos, 338 sr->read_size - sr->read_pos); 339 if (ret < 0) 340 { 341 if ( (EAGAIN != errno) && 342 (EWOULDBLOCK != errno) && 343 (EINTR != errno) ) 344 { 345 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 346 "read"); 347 return; 348 } 349 /* Continue later */ 350 break; 351 } 352 if (0 == ret) 353 { 354 /* regular end of stream, odd! */ 355 fail_hard (sr); 356 return; 357 } 358 GNUNET_assert (sr->read_size >= sr->read_pos + ret); 359 sr->read_pos += ret; 360 if (! process_buffer (sr)) 361 return; 362 } 363 sr->read_task 364 = GNUNET_SCHEDULER_add_read_file ( 365 GNUNET_TIME_UNIT_FOREVER_REL, 366 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 write_cb (void *cls) 380 { 381 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 382 struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail; 383 ssize_t ret; 384 385 sr->write_task = NULL; 386 while ( (NULL != ee) && 387 (ee->write_size == ee->write_pos) ) 388 ee = ee->prev; 389 while (NULL != ee) 390 { 391 while (ee->write_size > ee->write_pos) 392 { 393 ret = GNUNET_DISK_file_write (sr->chld_stdin, 394 ee->write_buf + ee->write_pos, 395 ee->write_size - ee->write_pos); 396 if (ret < 0) 397 { 398 if ( (EAGAIN != errno) && 399 (EINTR != errno) ) 400 { 401 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 402 "write"); 403 /* helper must have died */ 404 fail_hard (sr); 405 return; 406 } 407 break; 408 } 409 if (0 == ret) 410 { 411 GNUNET_break (0); 412 break; 413 } 414 GNUNET_assert (ee->write_size >= ee->write_pos + ret); 415 ee->write_pos += ret; 416 } 417 if ( (ee->write_size > ee->write_pos) && 418 ( (EAGAIN == errno) || 419 (EWOULDBLOCK == errno) || 420 (EINTR == errno) ) ) 421 { 422 sr->write_task 423 = GNUNET_SCHEDULER_add_write_file ( 424 GNUNET_TIME_UNIT_FOREVER_REL, 425 sr->chld_stdin, 426 &write_cb, 427 sr); 428 return; 429 } 430 if (ee->write_size == ee->write_pos) 431 { 432 GNUNET_free (ee->write_buf); 433 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 child_done_cb (void *cls, 450 enum GNUNET_OS_ProcessStatusType type, 451 long unsigned int exit_code) 452 { 453 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 454 455 sr->cwh = NULL; 456 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 fail_hard (sr); 462 } 463 464 465 struct TALER_KYCLOGIC_SanctionRater * 466 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 sr = GNUNET_new (struct TALER_KYCLOGIC_SanctionRater); 474 pipe_stdin = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_READ); 475 GNUNET_assert (NULL != pipe_stdin); 476 pipe_stdout = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_WRITE); 477 GNUNET_assert (NULL != pipe_stdout); 478 sr->helper = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 479 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 if (GNUNET_OK != 487 GNUNET_process_run_command_argv (sr->helper, 488 binary, 489 (const char **) argv)) 490 { 491 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 492 "Failed to run conversion helper `%s'\n", 493 binary); 494 GNUNET_process_destroy (sr->helper); 495 GNUNET_break (GNUNET_OK == 496 GNUNET_DISK_pipe_close (pipe_stdin)); 497 GNUNET_break (GNUNET_OK == 498 GNUNET_DISK_pipe_close (pipe_stdout)); 499 GNUNET_free (sr); 500 return NULL; 501 } 502 sr->chld_stdin = 503 GNUNET_DISK_pipe_detach_end (pipe_stdin, 504 GNUNET_DISK_PIPE_END_WRITE); 505 sr->chld_stdout = 506 GNUNET_DISK_pipe_detach_end (pipe_stdout, 507 GNUNET_DISK_PIPE_END_READ); 508 GNUNET_break (GNUNET_OK == 509 GNUNET_DISK_pipe_close (pipe_stdin)); 510 GNUNET_break (GNUNET_OK == 511 GNUNET_DISK_pipe_close (pipe_stdout)); 512 513 sr->read_task 514 = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, 515 sr->chld_stdout, 516 &read_cb, 517 sr); 518 sr->cwh = GNUNET_wait_child (sr->helper, 519 &child_done_cb, 520 sr); 521 return sr; 522 } 523 524 525 struct TALER_KYCLOGIC_EvaluationEntry * 526 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 if (NULL == sr->read_task) 535 return NULL; 536 ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry); 537 ee->cb = cb; 538 ee->cb_cls = cb_cls; 539 GNUNET_CONTAINER_DLL_insert (sr->ee_head, 540 sr->ee_tail, 541 ee); 542 js = json_dumps (attributes, 543 JSON_COMPACT); 544 GNUNET_asprintf (&ee->write_buf, 545 "%s\n", 546 js); 547 free (js); 548 ee->write_size = strlen (ee->write_buf); 549 if (NULL == sr->write_task) 550 sr->write_task 551 = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL, 552 sr->chld_stdin, 553 &write_cb, 554 sr); 555 return ee; 556 } 557 558 559 void 560 TALER_KYCLOGIC_sanction_rater_stop ( 561 struct TALER_KYCLOGIC_SanctionRater *sr) 562 { 563 fail_hard (sr); 564 if (NULL != sr->cwh) 565 { 566 GNUNET_wait_child_cancel (sr->cwh); 567 sr->cwh = NULL; 568 } 569 if (NULL != sr->write_task) 570 { 571 GNUNET_SCHEDULER_cancel (sr->write_task); 572 sr->write_task = NULL; 573 } 574 if (NULL != sr->chld_stdout) 575 { 576 GNUNET_break (GNUNET_OK == 577 GNUNET_DISK_file_close (sr->chld_stdout)); 578 sr->chld_stdout = NULL; 579 } 580 GNUNET_free (sr->read_buf); 581 GNUNET_free (sr); 582 }