testing_api_cmd_auditor_exec_auditor.c (13881B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2018, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it 6 under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 TALER is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with TALER; see the file COPYING. If not, 17 see <http://www.gnu.org/licenses/> 18 */ 19 /** 20 * @file testing/testing_api_cmd_auditor_exec_auditor.c 21 * @brief run all taler-helper-auditor-* programs and check that 22 * taler-auditor-httpd reports no findings 23 * @author Marcello Stanisci 24 * @author Christian Grothoff 25 */ 26 #include "platform.h" 27 #include "taler/taler_json_lib.h" 28 #include <gnunet/gnunet_curl_lib.h> 29 #include "taler/taler_testing_lib.h" 30 31 32 /** 33 * Auditor helpers to run, in this order. Each is run in test mode 34 * ("-t"), so it audits everything the exchange database holds so far 35 * and then exits. 36 */ 37 static const struct 38 { 39 /** 40 * Name of the helper binary. 41 */ 42 const char *name; 43 44 /** 45 * True if the helper reads the bank's history of the exchange's 46 * accounts. Test configurations may list accounts the bank does 47 * not have, so those helpers are told to skip them ("-I"). 48 */ 49 bool wire; 50 } helpers[] = { 51 { "taler-helper-auditor-aggregation", false }, 52 { "taler-helper-auditor-aml", false }, 53 { "taler-helper-auditor-coins", false }, 54 { "taler-helper-auditor-deposits", false }, 55 { "taler-helper-auditor-purses", false }, 56 { "taler-helper-auditor-reserves", false }, 57 { "taler-helper-auditor-transfer", false }, 58 { "taler-helper-auditor-wire-credit", true }, 59 { "taler-helper-auditor-wire-debit", true }, 60 { NULL, false } 61 }; 62 63 64 /** 65 * /monitoring/ reports of taler-auditor-httpd that must be empty after a 66 * correct exchange was audited. Not listed are the balances, progress 67 * and per-reserve/per-purse summaries, which are not findings, and 68 * "pending-deposits" and "aml-holds", which list payouts the exchange has 69 * not made *yet* (deposits before their wire deadline, transfers held 70 * for KYC) and thus depend on when the test last ran the aggregator. 71 */ 72 static const char *const reports[] = { 73 "amount-arithmetic-inconsistencies", 74 "bad-sig-losses", 75 "closure-lags", 76 "coin-inconsistencies", 77 "denomination-key-validity-withdraw-inconsistencies", 78 "denominations-without-sigs", 79 "deposit-confirmations", 80 "early-aggregations", 81 "emergencies", 82 "emergencies-by-count", 83 "fee-time-inconsistencies", 84 "kycauth-in-inconsistencies", 85 "misattribution-in-inconsistencies", 86 "purse-not-closed-inconsistencies", 87 "reserve-balance-insufficient-inconsistencies", 88 "reserve-balance-summary-wrong-inconsistencies", 89 "reserve-in-inconsistencies", 90 "reserve-not-closed-inconsistencies", 91 "row-inconsistencies", 92 "row-minor-inconsistencies", 93 "wire-format-inconsistencies", 94 "wire-out-inconsistencies", 95 NULL 96 }; 97 98 99 /** 100 * State for a "auditor" CMD. 101 */ 102 struct AuditorState 103 { 104 105 /** 106 * Our interpreter. 107 */ 108 struct TALER_TESTING_Interpreter *is; 109 110 /** 111 * Process of the helper currently running. 112 */ 113 struct GNUNET_Process *helper_proc; 114 115 /** 116 * Handle to wait for @e helper_proc to terminate. 117 */ 118 struct GNUNET_ChildWaitHandle *cwh; 119 120 /** 121 * Task to start the next helper. 122 */ 123 struct GNUNET_SCHEDULER_Task *task; 124 125 /** 126 * Request for a report, or NULL. 127 */ 128 struct GNUNET_CURL_Job *job; 129 130 /** 131 * Headers to send with each request for a report. 132 */ 133 struct curl_slist *job_headers; 134 135 /** 136 * Configuration file used by the command. 137 */ 138 const char *config_filename; 139 140 /** 141 * Base URL of the auditor, from @e config_filename. 142 */ 143 char *auditor_url; 144 145 /** 146 * Index into #helpers of the helper currently running. 147 */ 148 unsigned int helper_off; 149 150 /** 151 * Index into #reports of the report currently requested. 152 */ 153 unsigned int report_off; 154 155 /** 156 * Number of reports that were not empty. 157 */ 158 unsigned int findings; 159 }; 160 161 162 /** 163 * Request the next report from #reports, or finish the command 164 * if all of them were checked. 165 * 166 * @param as our state 167 */ 168 static void 169 check_next_report (struct AuditorState *as); 170 171 172 /** 173 * Called with a report from the auditor. 174 * 175 * @param cls our `struct AuditorState` 176 * @param response_code HTTP status returned by the auditor 177 * @param response response body, NULL if not in JSON 178 */ 179 static void 180 report_cb (void *cls, 181 long response_code, 182 const void *response) 183 { 184 struct AuditorState *as = cls; 185 const json_t *reply = response; 186 const char *report = reports[as->report_off]; 187 const json_t *records; 188 189 as->job = NULL; 190 if (MHD_HTTP_OK != response_code) 191 { 192 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 193 "Auditor returned HTTP status %u for report `%s'\n", 194 (unsigned int) response_code, 195 report); 196 TALER_TESTING_interpreter_fail (as->is); 197 return; 198 } 199 records = json_object_get (reply, 200 "records"); 201 if (! json_is_array (records)) 202 { 203 GNUNET_break_op (0); 204 TALER_TESTING_interpreter_fail (as->is); 205 return; 206 } 207 if (0 != json_array_size (records)) 208 { 209 char *s; 210 211 s = json_dumps (records, 212 JSON_COMPACT); 213 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 214 "Auditor reports %u %s: %s\n", 215 (unsigned int) json_array_size (records), 216 report, 217 s); 218 free (s); 219 as->findings++; 220 } 221 as->report_off++; 222 check_next_report (as); 223 } 224 225 226 static void 227 check_next_report (struct AuditorState *as) 228 { 229 CURL *eh; 230 char *url; 231 232 if (NULL == reports[as->report_off]) 233 { 234 if (0 != as->findings) 235 { 236 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 237 "Auditor has findings in %u reports\n", 238 as->findings); 239 TALER_TESTING_interpreter_fail (as->is); 240 return; 241 } 242 TALER_TESTING_interpreter_next (as->is); 243 return; 244 } 245 GNUNET_asprintf (&url, 246 "%smonitoring/%s", 247 as->auditor_url, 248 reports[as->report_off]); 249 eh = curl_easy_init (); 250 GNUNET_assert (NULL != eh); 251 GNUNET_assert (CURLE_OK == 252 curl_easy_setopt (eh, 253 CURLOPT_URL, 254 url)); 255 GNUNET_free (url); 256 as->job = GNUNET_CURL_job_add2 (TALER_TESTING_interpreter_get_context ( 257 as->is), 258 eh, 259 as->job_headers, 260 &report_cb, 261 as); 262 if (NULL == as->job) 263 { 264 GNUNET_break (0); 265 TALER_TESTING_interpreter_fail (as->is); 266 return; 267 } 268 } 269 270 271 /** 272 * Start the next helper from #helpers, or start checking 273 * the reports if all helpers have run. 274 * 275 * @param cls our `struct AuditorState` 276 */ 277 static void 278 run_next_helper (void *cls); 279 280 281 /** 282 * Called when the helper we started has terminated. 283 * 284 * @param cls our `struct AuditorState` 285 * @param type how the helper terminated 286 * @param code exit code of the helper 287 */ 288 static void 289 helper_done (void *cls, 290 enum GNUNET_OS_ProcessStatusType type, 291 long unsigned int code) 292 { 293 struct AuditorState *as = cls; 294 295 as->cwh = NULL; 296 GNUNET_process_destroy (as->helper_proc); 297 as->helper_proc = NULL; 298 if ( (GNUNET_OS_PROCESS_EXITED != type) || 299 (0 != code) ) 300 { 301 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 302 "%s failed (status %d, code %lu)\n", 303 helpers[as->helper_off].name, 304 (int) type, 305 code); 306 TALER_TESTING_interpreter_fail (as->is); 307 return; 308 } 309 as->helper_off++; 310 /* Not directly: GNUNET_wait_child() must not be called from 311 within a GNUNET_ChildCompletedCallback. */ 312 as->task = GNUNET_SCHEDULER_add_now (&run_next_helper, 313 as); 314 } 315 316 317 static void 318 run_next_helper (void *cls) 319 { 320 struct AuditorState *as = cls; 321 const char *helper = helpers[as->helper_off].name; 322 323 as->task = NULL; 324 if (NULL == helper) 325 { 326 check_next_report (as); 327 return; 328 } 329 as->helper_proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 330 if (GNUNET_OK != 331 GNUNET_process_run_command_va (as->helper_proc, 332 helper, 333 helper, 334 "-c", as->config_filename, 335 "-L", "INFO", 336 "-i", /* we audit the exchange DB directly */ 337 "-t", /* exit when done */ 338 helpers[as->helper_off].wire 339 ? "-I" /* ignore unknown accounts */ 340 : NULL, 341 NULL)) 342 { 343 GNUNET_break (0); 344 GNUNET_process_destroy (as->helper_proc); 345 as->helper_proc = NULL; 346 TALER_TESTING_interpreter_fail (as->is); 347 return; 348 } 349 as->cwh = GNUNET_wait_child (as->helper_proc, 350 &helper_done, 351 as); 352 } 353 354 355 /** 356 * Run the command; runs all auditor helpers against the exchange 357 * database and then asks the auditor for its findings. 358 * 359 * @param cls closure. 360 * @param cmd the commaind being run. 361 * @param is interpreter state. 362 */ 363 static void 364 auditor_run (void *cls, 365 const struct TALER_TESTING_Command *cmd, 366 struct TALER_TESTING_Interpreter *is) 367 { 368 struct AuditorState *as = cls; 369 struct GNUNET_CONFIGURATION_Handle *cfg; 370 371 (void) cmd; 372 as->is = is; 373 cfg = GNUNET_CONFIGURATION_create (TALER_AUDITOR_project_data ()); 374 if (GNUNET_OK != 375 GNUNET_CONFIGURATION_load (cfg, 376 as->config_filename)) 377 { 378 GNUNET_break (0); 379 GNUNET_CONFIGURATION_destroy (cfg); 380 TALER_TESTING_interpreter_fail (is); 381 return; 382 } 383 if (GNUNET_OK != 384 GNUNET_CONFIGURATION_get_value_string (cfg, 385 "auditor", 386 "BASE_URL", 387 &as->auditor_url)) 388 { 389 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 390 "auditor", 391 "BASE_URL"); 392 GNUNET_CONFIGURATION_destroy (cfg); 393 TALER_TESTING_interpreter_fail (is); 394 return; 395 } 396 { 397 const char *tok = getenv ("TALER_AUDITOR_ACCESS_TOKEN"); 398 char *cfg_tok = NULL; 399 400 /* Find the token the way taler-auditor-httpd does: the environment 401 variable overrides the configuration, and the token may be stored 402 with or without the RFC 8959 prefix. If there is none, the httpd 403 must have been started with -n. */ 404 if ( (NULL == tok) && 405 (GNUNET_OK == 406 GNUNET_CONFIGURATION_get_value_string (cfg, 407 "auditor", 408 "ACCESS_TOKEN", 409 &cfg_tok)) ) 410 tok = cfg_tok; 411 if (NULL != tok) 412 { 413 char *hdr; 414 415 if (0 != strncasecmp (tok, 416 "secret-token:", 417 strlen ("secret-token:"))) 418 { 419 GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, 420 "auditor", 421 "ACCESS_TOKEN", 422 "lacks RFC 8959 prefix"); 423 GNUNET_CONFIGURATION_destroy (cfg); 424 TALER_TESTING_interpreter_fail (is); 425 return; 426 } 427 tok += strlen ("secret-token:"); 428 GNUNET_asprintf (&hdr, 429 "%s: Bearer secret-token:%s", 430 MHD_HTTP_HEADER_AUTHORIZATION, 431 tok); 432 as->job_headers = curl_slist_append (as->job_headers, 433 hdr); 434 GNUNET_free (hdr); 435 } 436 GNUNET_free (cfg_tok); 437 } 438 GNUNET_CONFIGURATION_destroy (cfg); 439 run_next_helper (as); 440 } 441 442 443 /** 444 * Free the state of a "auditor" CMD, and possibly kills its 445 * process if it did not terminate correctly. 446 * 447 * @param cls closure. 448 * @param cmd the command being freed. 449 */ 450 static void 451 auditor_cleanup (void *cls, 452 const struct TALER_TESTING_Command *cmd) 453 { 454 struct AuditorState *as = cls; 455 456 (void) cmd; 457 if (NULL != as->task) 458 { 459 GNUNET_SCHEDULER_cancel (as->task); 460 as->task = NULL; 461 } 462 if (NULL != as->cwh) 463 { 464 GNUNET_wait_child_cancel (as->cwh); 465 as->cwh = NULL; 466 } 467 if (NULL != as->helper_proc) 468 { 469 GNUNET_break (GNUNET_OK == 470 GNUNET_process_kill (as->helper_proc, 471 SIGKILL)); 472 GNUNET_process_wait (as->helper_proc, 473 true, 474 NULL, 475 NULL); 476 GNUNET_process_destroy (as->helper_proc); 477 as->helper_proc = NULL; 478 } 479 if (NULL != as->job) 480 { 481 GNUNET_CURL_job_cancel (as->job); 482 as->job = NULL; 483 } 484 curl_slist_free_all (as->job_headers); 485 GNUNET_free (as->auditor_url); 486 GNUNET_free (as); 487 } 488 489 490 struct TALER_TESTING_Command 491 TALER_TESTING_cmd_exec_auditor (const char *label, 492 const char *config_filename) 493 { 494 struct AuditorState *as; 495 496 as = GNUNET_new (struct AuditorState); 497 as->config_filename = config_filename; 498 { 499 struct TALER_TESTING_Command cmd = { 500 .cls = as, 501 .label = label, 502 .run = &auditor_run, 503 .cleanup = &auditor_cleanup 504 }; 505 506 return cmd; 507 } 508 } 509 510 511 /* end of testing_api_cmd_auditor_exec_auditor.c */