Line data Source code
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 132 : report_cb (void *cls,
181 : long response_code,
182 : const void *response)
183 : {
184 132 : struct AuditorState *as = cls;
185 132 : const json_t *reply = response;
186 132 : const char *report = reports[as->report_off];
187 : const json_t *records;
188 :
189 132 : as->job = NULL;
190 132 : if (MHD_HTTP_OK != response_code)
191 : {
192 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
193 : "Auditor returned HTTP status %u for report `%s'\n",
194 : (unsigned int) response_code,
195 : report);
196 0 : TALER_TESTING_interpreter_fail (as->is);
197 0 : return;
198 : }
199 132 : records = json_object_get (reply,
200 : "records");
201 132 : if (! json_is_array (records))
202 : {
203 0 : GNUNET_break_op (0);
204 0 : TALER_TESTING_interpreter_fail (as->is);
205 0 : return;
206 : }
207 132 : if (0 != json_array_size (records))
208 : {
209 : char *s;
210 :
211 0 : s = json_dumps (records,
212 : JSON_COMPACT);
213 0 : 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 0 : free (s);
219 0 : as->findings++;
220 : }
221 132 : as->report_off++;
222 132 : check_next_report (as);
223 : }
224 :
225 :
226 : static void
227 138 : check_next_report (struct AuditorState *as)
228 : {
229 : CURL *eh;
230 : char *url;
231 :
232 138 : if (NULL == reports[as->report_off])
233 : {
234 6 : if (0 != as->findings)
235 : {
236 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
237 : "Auditor has findings in %u reports\n",
238 : as->findings);
239 0 : TALER_TESTING_interpreter_fail (as->is);
240 6 : return;
241 : }
242 6 : TALER_TESTING_interpreter_next (as->is);
243 6 : return;
244 : }
245 132 : GNUNET_asprintf (&url,
246 : "%smonitoring/%s",
247 : as->auditor_url,
248 132 : reports[as->report_off]);
249 132 : eh = curl_easy_init ();
250 132 : GNUNET_assert (NULL != eh);
251 132 : GNUNET_assert (CURLE_OK ==
252 : curl_easy_setopt (eh,
253 : CURLOPT_URL,
254 : url));
255 132 : GNUNET_free (url);
256 132 : as->job = GNUNET_CURL_job_add2 (TALER_TESTING_interpreter_get_context (
257 : as->is),
258 : eh,
259 132 : as->job_headers,
260 : &report_cb,
261 : as);
262 132 : if (NULL == as->job)
263 : {
264 0 : GNUNET_break (0);
265 0 : TALER_TESTING_interpreter_fail (as->is);
266 0 : 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 54 : helper_done (void *cls,
290 : enum GNUNET_OS_ProcessStatusType type,
291 : long unsigned int code)
292 : {
293 54 : struct AuditorState *as = cls;
294 :
295 54 : as->cwh = NULL;
296 54 : GNUNET_process_destroy (as->helper_proc);
297 54 : as->helper_proc = NULL;
298 54 : if ( (GNUNET_OS_PROCESS_EXITED != type) ||
299 : (0 != code) )
300 : {
301 0 : 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 0 : TALER_TESTING_interpreter_fail (as->is);
307 0 : return;
308 : }
309 54 : as->helper_off++;
310 : /* Not directly: GNUNET_wait_child() must not be called from
311 : within a GNUNET_ChildCompletedCallback. */
312 54 : as->task = GNUNET_SCHEDULER_add_now (&run_next_helper,
313 : as);
314 : }
315 :
316 :
317 : static void
318 60 : run_next_helper (void *cls)
319 : {
320 60 : struct AuditorState *as = cls;
321 60 : const char *helper = helpers[as->helper_off].name;
322 :
323 60 : as->task = NULL;
324 60 : if (NULL == helper)
325 : {
326 6 : check_next_report (as);
327 6 : return;
328 : }
329 54 : as->helper_proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
330 54 : if (GNUNET_OK !=
331 54 : 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 54 : helpers[as->helper_off].wire
339 : ? "-I" /* ignore unknown accounts */
340 : : NULL,
341 : NULL))
342 : {
343 0 : GNUNET_break (0);
344 0 : GNUNET_process_destroy (as->helper_proc);
345 0 : as->helper_proc = NULL;
346 0 : TALER_TESTING_interpreter_fail (as->is);
347 0 : return;
348 : }
349 54 : 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 6 : auditor_run (void *cls,
365 : const struct TALER_TESTING_Command *cmd,
366 : struct TALER_TESTING_Interpreter *is)
367 : {
368 6 : struct AuditorState *as = cls;
369 : struct GNUNET_CONFIGURATION_Handle *cfg;
370 :
371 : (void) cmd;
372 6 : as->is = is;
373 6 : cfg = GNUNET_CONFIGURATION_create (TALER_AUDITOR_project_data ());
374 6 : if (GNUNET_OK !=
375 6 : GNUNET_CONFIGURATION_load (cfg,
376 : as->config_filename))
377 : {
378 0 : GNUNET_break (0);
379 0 : GNUNET_CONFIGURATION_destroy (cfg);
380 0 : TALER_TESTING_interpreter_fail (is);
381 0 : return;
382 : }
383 6 : if (GNUNET_OK !=
384 6 : GNUNET_CONFIGURATION_get_value_string (cfg,
385 : "auditor",
386 : "BASE_URL",
387 : &as->auditor_url))
388 : {
389 0 : GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
390 : "auditor",
391 : "BASE_URL");
392 0 : GNUNET_CONFIGURATION_destroy (cfg);
393 0 : TALER_TESTING_interpreter_fail (is);
394 0 : return;
395 : }
396 : {
397 6 : const char *tok = getenv ("TALER_AUDITOR_ACCESS_TOKEN");
398 6 : 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 12 : if ( (NULL == tok) &&
405 : (GNUNET_OK ==
406 6 : GNUNET_CONFIGURATION_get_value_string (cfg,
407 : "auditor",
408 : "ACCESS_TOKEN",
409 : &cfg_tok)) )
410 6 : tok = cfg_tok;
411 6 : if (NULL != tok)
412 : {
413 : char *hdr;
414 :
415 6 : if (0 != strncasecmp (tok,
416 : "secret-token:",
417 : strlen ("secret-token:")))
418 : {
419 0 : GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
420 : "auditor",
421 : "ACCESS_TOKEN",
422 : "lacks RFC 8959 prefix");
423 0 : GNUNET_CONFIGURATION_destroy (cfg);
424 0 : TALER_TESTING_interpreter_fail (is);
425 0 : return;
426 : }
427 6 : tok += strlen ("secret-token:");
428 6 : GNUNET_asprintf (&hdr,
429 : "%s: Bearer secret-token:%s",
430 : MHD_HTTP_HEADER_AUTHORIZATION,
431 : tok);
432 6 : as->job_headers = curl_slist_append (as->job_headers,
433 : hdr);
434 6 : GNUNET_free (hdr);
435 : }
436 6 : GNUNET_free (cfg_tok);
437 : }
438 6 : GNUNET_CONFIGURATION_destroy (cfg);
439 6 : 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 6 : auditor_cleanup (void *cls,
452 : const struct TALER_TESTING_Command *cmd)
453 : {
454 6 : struct AuditorState *as = cls;
455 :
456 : (void) cmd;
457 6 : if (NULL != as->task)
458 : {
459 0 : GNUNET_SCHEDULER_cancel (as->task);
460 0 : as->task = NULL;
461 : }
462 6 : if (NULL != as->cwh)
463 : {
464 0 : GNUNET_wait_child_cancel (as->cwh);
465 0 : as->cwh = NULL;
466 : }
467 6 : if (NULL != as->helper_proc)
468 : {
469 0 : GNUNET_break (GNUNET_OK ==
470 : GNUNET_process_kill (as->helper_proc,
471 : SIGKILL));
472 0 : GNUNET_process_wait (as->helper_proc,
473 : true,
474 : NULL,
475 : NULL);
476 0 : GNUNET_process_destroy (as->helper_proc);
477 0 : as->helper_proc = NULL;
478 : }
479 6 : if (NULL != as->job)
480 : {
481 0 : GNUNET_CURL_job_cancel (as->job);
482 0 : as->job = NULL;
483 : }
484 6 : curl_slist_free_all (as->job_headers);
485 6 : GNUNET_free (as->auditor_url);
486 6 : GNUNET_free (as);
487 6 : }
488 :
489 :
490 : struct TALER_TESTING_Command
491 6 : TALER_TESTING_cmd_exec_auditor (const char *label,
492 : const char *config_filename)
493 : {
494 : struct AuditorState *as;
495 :
496 6 : as = GNUNET_new (struct AuditorState);
497 6 : as->config_filename = config_filename;
498 : {
499 6 : struct TALER_TESTING_Command cmd = {
500 : .cls = as,
501 : .label = label,
502 : .run = &auditor_run,
503 : .cleanup = &auditor_cleanup
504 : };
505 :
506 6 : return cmd;
507 : }
508 : }
509 :
510 :
511 : /* end of testing_api_cmd_auditor_exec_auditor.c */
|