kyclogic_sanctions.c (14322B)
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_OS_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_OS_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_OS_start_process_vap (GNUNET_OS_INHERIT_STD_ERR, 468 pipe_stdin, 469 pipe_stdout, 470 NULL, 471 binary, 472 (char *const *) argv); 473 if (NULL == sr->helper) 474 { 475 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 476 "Failed to run conversion helper `%s'\n", 477 binary); 478 GNUNET_break (GNUNET_OK == 479 GNUNET_DISK_pipe_close (pipe_stdin)); 480 GNUNET_break (GNUNET_OK == 481 GNUNET_DISK_pipe_close (pipe_stdout)); 482 GNUNET_free (sr); 483 return NULL; 484 } 485 sr->chld_stdin = 486 GNUNET_DISK_pipe_detach_end (pipe_stdin, 487 GNUNET_DISK_PIPE_END_WRITE); 488 sr->chld_stdout = 489 GNUNET_DISK_pipe_detach_end (pipe_stdout, 490 GNUNET_DISK_PIPE_END_READ); 491 GNUNET_break (GNUNET_OK == 492 GNUNET_DISK_pipe_close (pipe_stdin)); 493 GNUNET_break (GNUNET_OK == 494 GNUNET_DISK_pipe_close (pipe_stdout)); 495 496 sr->read_task 497 = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, 498 sr->chld_stdout, 499 &read_cb, 500 sr); 501 sr->cwh = GNUNET_wait_child (sr->helper, 502 &child_done_cb, 503 sr); 504 return sr; 505 } 506 507 508 struct TALER_KYCLOGIC_EvaluationEntry * 509 TALER_KYCLOGIC_sanction_rater_eval (struct TALER_KYCLOGIC_SanctionRater *sr, 510 const json_t *attributes, 511 TALER_KYCLOGIC_SanctionResultCallback cb, 512 void *cb_cls) 513 { 514 struct TALER_KYCLOGIC_EvaluationEntry *ee; 515 char *js; 516 517 if (NULL == sr->read_task) 518 return NULL; 519 ee = GNUNET_new (struct TALER_KYCLOGIC_EvaluationEntry); 520 ee->cb = cb; 521 ee->cb_cls = cb_cls; 522 GNUNET_CONTAINER_DLL_insert (sr->ee_head, 523 sr->ee_tail, 524 ee); 525 js = json_dumps (attributes, 526 JSON_COMPACT); 527 GNUNET_asprintf (&ee->write_buf, 528 "%s\n", 529 js); 530 free (js); 531 ee->write_size = strlen (ee->write_buf); 532 if (NULL == sr->write_task) 533 sr->write_task 534 = GNUNET_SCHEDULER_add_write_file (GNUNET_TIME_UNIT_FOREVER_REL, 535 sr->chld_stdin, 536 &write_cb, 537 sr); 538 return ee; 539 } 540 541 542 void 543 TALER_KYCLOGIC_sanction_rater_stop ( 544 struct TALER_KYCLOGIC_SanctionRater *sr) 545 { 546 fail_hard (sr); 547 if (NULL != sr->cwh) 548 { 549 GNUNET_wait_child_cancel (sr->cwh); 550 sr->cwh = NULL; 551 } 552 if (NULL != sr->write_task) 553 { 554 GNUNET_SCHEDULER_cancel (sr->write_task); 555 sr->write_task = NULL; 556 } 557 if (NULL != sr->chld_stdout) 558 { 559 GNUNET_break (GNUNET_OK == 560 GNUNET_DISK_file_close (sr->chld_stdout)); 561 sr->chld_stdout = NULL; 562 } 563 GNUNET_free (sr->read_buf); 564 GNUNET_free (sr); 565 }