kyclogic_sanctions.c (14612B)
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 "taler/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->helper) 156 { 157 GNUNET_process_destroy (sr->helper); 158 sr->helper = NULL; 159 } 160 while (NULL != (ee = sr->ee_tail)) 161 { 162 GNUNET_CONTAINER_DLL_remove (sr->ee_head, 163 sr->ee_tail, 164 ee); 165 ee->cb (ee->cb_cls, 166 TALER_EC_EXCHANGE_GENERIC_KYC_SANCTION_LIST_CHECK_FAILED, 167 NULL, 168 1.0, 169 0.0); 170 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 171 "Failed to send %u bytes to child\n", 172 (unsigned int) (ee->write_size - ee->write_pos)); 173 GNUNET_free (ee->write_buf); 174 GNUNET_free (ee); 175 } 176 } 177 178 179 /** 180 * Parse data from input buffer. 181 * 182 * @param[in,out] sr sanction rater to process data from 183 * @return true if everything is fine, false on failure 184 */ 185 static bool 186 process_buffer (struct TALER_KYCLOGIC_SanctionRater *sr) 187 { 188 const char *buf = sr->read_buf; 189 size_t buf_len; 190 void *end; 191 192 end = memrchr (sr->read_buf, 193 '\n', 194 sr->read_pos); 195 if ( (NULL == end) && 196 (sr->read_pos < 2048) ) 197 return true; 198 if (NULL == end) 199 { 200 /* line returned by sanction rater way too long */ 201 GNUNET_break (0); 202 return false; 203 } 204 end++; 205 buf_len = end - sr->read_buf; 206 while (0 != buf_len) 207 { 208 char *nl; 209 double rating; 210 double confidence; 211 char best_match[1024]; 212 size_t line_len; 213 214 nl = memchr (buf, 215 '\n', 216 buf_len); 217 if (NULL == nl) 218 { 219 /* no newline in 2048 bytes? not allowed */ 220 GNUNET_break (0); 221 return false; 222 } 223 *nl = '\0'; 224 line_len = nl - buf + 1; 225 if (3 != 226 sscanf (buf, 227 "%lf %lf %1023s", 228 &rating, 229 &confidence, 230 best_match)) 231 { 232 /* maybe best_match is empty because literally nothing matched */ 233 if (2 != 234 sscanf (buf, 235 "%lf %lf ", 236 &rating, 237 &confidence)) 238 { 239 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 240 "Malformed input line `%s'\n", 241 buf); 242 GNUNET_break (0); 243 return false; 244 } 245 strcpy (best_match, 246 "<none>"); 247 } 248 { 249 struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail; 250 251 GNUNET_CONTAINER_DLL_remove (sr->ee_head, 252 sr->ee_tail, 253 ee); 254 ee->cb (ee->cb_cls, 255 TALER_EC_NONE, 256 best_match, 257 rating, 258 confidence); 259 GNUNET_free (ee->write_buf); 260 GNUNET_free (ee); 261 } 262 buf += line_len; 263 buf_len -= line_len; 264 } 265 buf_len = end - sr->read_buf; 266 memmove (sr->read_buf, 267 end, 268 sr->read_pos - buf_len); 269 sr->read_pos -= buf_len; 270 return true; 271 } 272 273 274 /** 275 * Function called when we can read more data from 276 * the child process. 277 * 278 * @param cls our `struct TALER_KYCLOGIC_SanctionRater *` 279 */ 280 static void 281 read_cb (void *cls) 282 { 283 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 284 285 sr->read_task = NULL; 286 while (1) 287 { 288 ssize_t ret; 289 290 if (sr->read_size == sr->read_pos) 291 { 292 /* Grow input buffer */ 293 size_t ns; 294 void *tmp; 295 296 ns = GNUNET_MAX (2 * sr->read_size, 297 1024); 298 if (ns > GNUNET_MAX_MALLOC_CHECKED) 299 ns = GNUNET_MAX_MALLOC_CHECKED; 300 if (sr->read_size == ns) 301 { 302 /* Helper returned more than 40 MB of data! Stop reading! */ 303 GNUNET_break (0); 304 GNUNET_break (GNUNET_OK == 305 GNUNET_DISK_file_close (sr->chld_stdin)); 306 return; 307 } 308 tmp = GNUNET_malloc_large (ns); 309 if (NULL == tmp) 310 { 311 /* out of memory, also stop reading */ 312 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 313 "malloc"); 314 GNUNET_break (GNUNET_OK == 315 GNUNET_DISK_file_close (sr->chld_stdin)); 316 return; 317 } 318 GNUNET_memcpy (tmp, 319 sr->read_buf, 320 sr->read_pos); 321 GNUNET_free (sr->read_buf); 322 sr->read_buf = tmp; 323 sr->read_size = ns; 324 } 325 ret = GNUNET_DISK_file_read (sr->chld_stdout, 326 sr->read_buf + sr->read_pos, 327 sr->read_size - sr->read_pos); 328 if (ret < 0) 329 { 330 if ( (EAGAIN != errno) && 331 (EWOULDBLOCK != errno) && 332 (EINTR != errno) ) 333 { 334 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 335 "read"); 336 return; 337 } 338 /* Continue later */ 339 break; 340 } 341 if (0 == ret) 342 { 343 /* regular end of stream, odd! */ 344 fail_hard (sr); 345 return; 346 } 347 GNUNET_assert (sr->read_size >= sr->read_pos + ret); 348 sr->read_pos += ret; 349 if (! process_buffer (sr)) 350 return; 351 } 352 sr->read_task 353 = GNUNET_SCHEDULER_add_read_file ( 354 GNUNET_TIME_UNIT_FOREVER_REL, 355 sr->chld_stdout, 356 &read_cb, 357 sr); 358 } 359 360 361 /** 362 * Function called when we can write more data to 363 * the child process. 364 * 365 * @param cls our `struct SanctionRater *` 366 */ 367 static void 368 write_cb (void *cls) 369 { 370 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 371 struct TALER_KYCLOGIC_EvaluationEntry *ee = sr->ee_tail; 372 ssize_t ret; 373 374 sr->write_task = NULL; 375 while ( (NULL != ee) && 376 (ee->write_size == ee->write_pos) ) 377 ee = ee->prev; 378 while (NULL != ee) 379 { 380 while (ee->write_size > ee->write_pos) 381 { 382 ret = GNUNET_DISK_file_write (sr->chld_stdin, 383 ee->write_buf + ee->write_pos, 384 ee->write_size - ee->write_pos); 385 if (ret < 0) 386 { 387 if ( (EAGAIN != errno) && 388 (EINTR != errno) ) 389 { 390 GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, 391 "write"); 392 /* helper must have died */ 393 fail_hard (sr); 394 return; 395 } 396 break; 397 } 398 if (0 == ret) 399 { 400 GNUNET_break (0); 401 break; 402 } 403 GNUNET_assert (ee->write_size >= ee->write_pos + ret); 404 ee->write_pos += ret; 405 } 406 if ( (ee->write_size > ee->write_pos) && 407 ( (EAGAIN == errno) || 408 (EWOULDBLOCK == errno) || 409 (EINTR == errno) ) ) 410 { 411 sr->write_task 412 = GNUNET_SCHEDULER_add_write_file ( 413 GNUNET_TIME_UNIT_FOREVER_REL, 414 sr->chld_stdin, 415 &write_cb, 416 sr); 417 return; 418 } 419 if (ee->write_size == ee->write_pos) 420 { 421 GNUNET_free (ee->write_buf); 422 ee = ee->prev; 423 } 424 } /* while (NULL != ee) */ 425 } 426 427 428 /** 429 * Defines a GNUNET_ChildCompletedCallback which is sent back 430 * upon death or completion of a child process. 431 * 432 * @param cls handle for the callback 433 * @param type type of the process 434 * @param exit_code status code of the process 435 * 436 */ 437 static void 438 child_done_cb (void *cls, 439 enum GNUNET_OS_ProcessStatusType type, 440 long unsigned int exit_code) 441 { 442 struct TALER_KYCLOGIC_SanctionRater *sr = cls; 443 444 sr->cwh = NULL; 445 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 446 "Conversion helper exited with status %d and code %llu after outputting %llu bytes of data\n", 447 (int) type, 448 (unsigned long long) exit_code, 449 (unsigned long long) sr->read_pos); 450 fail_hard (sr); 451 } 452 453 454 struct TALER_KYCLOGIC_SanctionRater * 455 TALER_KYCLOGIC_sanction_rater_start (const char *binary, 456 char *const*argv) 457 { 458 struct TALER_KYCLOGIC_SanctionRater *sr; 459 struct GNUNET_DISK_PipeHandle *pipe_stdin; 460 struct GNUNET_DISK_PipeHandle *pipe_stdout; 461 462 sr = GNUNET_new (struct TALER_KYCLOGIC_SanctionRater); 463 pipe_stdin = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_READ); 464 GNUNET_assert (NULL != pipe_stdin); 465 pipe_stdout = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_WRITE); 466 GNUNET_assert (NULL != pipe_stdout); 467 sr->helper = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 468 GNUNET_assert (GNUNET_OK == 469 GNUNET_process_set_options ( 470 sr->helper, 471 GNUNET_process_option_inherit_rpipe (pipe_stdin, 472 STDIN_FILENO), 473 GNUNET_process_option_inherit_wpipe (pipe_stdout, 474 STDOUT_FILENO))); 475 if (GNUNET_OK != 476 GNUNET_process_run_command_argv (sr->helper, 477 binary, 478 (const char **) argv)) 479 { 480 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 481 "Failed to run conversion helper `%s'\n", 482 binary); 483 GNUNET_process_destroy (sr->helper); 484 GNUNET_break (GNUNET_OK == 485 GNUNET_DISK_pipe_close (pipe_stdin)); 486 GNUNET_break (GNUNET_OK == 487 GNUNET_DISK_pipe_close (pipe_stdout)); 488 GNUNET_free (sr); 489 return NULL; 490 } 491 sr->chld_stdin = 492 GNUNET_DISK_pipe_detach_end (pipe_stdin, 493 GNUNET_DISK_PIPE_END_WRITE); 494 sr->chld_stdout = 495 GNUNET_DISK_pipe_detach_end (pipe_stdout, 496 GNUNET_DISK_PIPE_END_READ); 497 GNUNET_break (GNUNET_OK == 498 GNUNET_DISK_pipe_close (pipe_stdin)); 499 GNUNET_break (GNUNET_OK == 500 GNUNET_DISK_pipe_close (pipe_stdout)); 501 502 sr->read_task 503 = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, 504 sr->chld_stdout, 505 &read_cb, 506 sr); 507 sr->cwh = GNUNET_wait_child (sr->helper, 508 &child_done_cb, 509 sr); 510 return sr; 511 } 512 513 514 struct TALER_KYCLOGIC_EvaluationEntry * 515 TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr, 516 const json_t *attributes, 517 TALER_KYCLOGIC_SanctionResultCallback cb, 518 void *cb_cls) 519 { 520 struct TALER_KYCLOGIC_EvaluationEntry *ee; 521 char *js; 522 523 if (NULL == sr->read_task) 524 return NULL; 525 ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry); 526 ee->cb = cb; 527 ee->cb_cls = cb_cls; 528 GNUNET_CONTAINER_DLL_insert (sr->ee_head, 529 sr->ee_tail, 530 ee); 531 js = json_dumps (attributes, 532 JSON_COMPACT); 533 GNUNET_asprintf (&ee->write_buf, 534 "%s\n", 535 js); 536 free (js); 537 ee->write_size = strlen (ee->write_buf); 538 if (NULL == sr->write_task) 539 sr->write_task 540 = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL, 541 sr->chld_stdin, 542 &write_cb, 543 sr); 544 return ee; 545 } 546 547 548 void 549 TALER_KYCLOGIC_sanction_rater_stop ( 550 struct TALER_KYCLOGIC_SanctionRater *sr) 551 { 552 fail_hard (sr); 553 if (NULL != sr->cwh) 554 { 555 GNUNET_wait_child_cancel (sr->cwh); 556 sr->cwh = NULL; 557 } 558 if (NULL != sr->write_task) 559 { 560 GNUNET_SCHEDULER_cancel (sr->write_task); 561 sr->write_task = NULL; 562 } 563 if (NULL != sr->chld_stdout) 564 { 565 GNUNET_break (GNUNET_OK == 566 GNUNET_DISK_file_close (sr->chld_stdout)); 567 sr->chld_stdout = NULL; 568 } 569 GNUNET_free (sr->read_buf); 570 GNUNET_free (sr); 571 }