merchant_api_get_statistics.c (22057B)
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 Lesser General Public License as published by the Free Software 7 Foundation; either version 2.1, 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 Lesser General Public License for more details. 12 13 You should have received a copy of the GNU Lesser General Public License along with 14 TALER; see the file COPYING.LGPL. If not, see 15 <http://www.gnu.org/licenses/> 16 */ 17 /** 18 * @file merchant_api_get_statistics.c 19 * @brief Implementation of the GET /statistics-[counter,amount]/$SLUG request of the merchant's HTTP API 20 * @author Martin Schanzenbach 21 */ 22 #include "platform.h" 23 #include <curl/curl.h> 24 #include <gnunet/gnunet_common.h> 25 #include <gnunet/gnunet_json_lib.h> 26 #include <jansson.h> 27 #include <microhttpd.h> /* just for HTTP status codes */ 28 #include <gnunet/gnunet_util_lib.h> 29 #include <gnunet/gnunet_curl_lib.h> 30 #include "taler_merchant_service.h" 31 #include "merchant_api_curl_defaults.h" 32 #include <taler/taler_json_lib.h> 33 #include <taler/taler_signatures.h> 34 35 /** 36 * Maximum number of statistics we return 37 */ 38 #define MAX_STATISTICS 1024 39 40 /** 41 * Handle for a GET /statistics-amount/$SLUG operation. 42 */ 43 struct TALER_MERCHANT_StatisticsAmountGetHandle 44 { 45 /** 46 * The url for this request. 47 */ 48 char *url; 49 50 /** 51 * Handle for the request. 52 */ 53 struct GNUNET_CURL_Job *job; 54 55 /** 56 * Function to call with the result. 57 */ 58 TALER_MERCHANT_StatisticsAmountGetCallback cb; 59 60 /** 61 * Closure for @a cb. 62 */ 63 void *cb_cls; 64 65 /** 66 * Reference to the execution context. 67 */ 68 struct GNUNET_CURL_Context *ctx; 69 70 }; 71 72 /** 73 * Handle for a GET /statistics-counter/$SLUG operation. 74 */ 75 struct TALER_MERCHANT_StatisticsCounterGetHandle 76 { 77 /** 78 * The url for this request. 79 */ 80 char *url; 81 82 /** 83 * Handle for the request. 84 */ 85 struct GNUNET_CURL_Job *job; 86 87 /** 88 * Function to call with the result. 89 */ 90 TALER_MERCHANT_StatisticsCounterGetCallback cb; 91 92 /** 93 * Closure for @a cb. 94 */ 95 void *cb_cls; 96 97 /** 98 * Reference to the execution context. 99 */ 100 struct GNUNET_CURL_Context *ctx; 101 102 }; 103 104 105 /** 106 * Parse interval information from buckets and intervals. 107 * 108 * @param json overall JSON reply 109 * @param jbuckets JSON array (or NULL!) with bucket data 110 * @param buckets_description human-readable description for the buckets 111 * @param jintervals JSON array (or NULL!) with bucket data 112 * @param intervals_description human-readable description for the intervals 113 * @param sgh operation handle 114 * @return #GNUNET_OK on success 115 */ 116 static enum GNUNET_GenericReturnValue 117 parse_intervals_and_buckets_amt ( 118 const json_t *json, 119 const json_t *jbuckets, 120 const char *buckets_description, 121 const json_t *jintervals, 122 const char *intervals_description, 123 struct TALER_MERCHANT_StatisticsAmountGetHandle *sgh 124 ) 125 { 126 unsigned int resp_buckets_len = json_array_size (jbuckets); 127 unsigned int resp_intervals_len = json_array_size (jintervals); 128 129 if ( (json_array_size (jbuckets) != (size_t) resp_buckets_len) || 130 (json_array_size (jintervals) != (size_t) resp_intervals_len) || 131 (resp_intervals_len = resp_buckets_len > MAX_STATISTICS) ) 132 { 133 GNUNET_break (0); 134 return GNUNET_SYSERR; 135 } 136 { 137 struct TALER_MERCHANT_StatisticAmountByBucket resp_buckets[ 138 GNUNET_NZL (resp_buckets_len)]; 139 struct TALER_MERCHANT_StatisticAmountByInterval resp_intervals[ 140 GNUNET_NZL (resp_intervals_len)]; 141 size_t index; 142 json_t *value; 143 enum GNUNET_GenericReturnValue ret; 144 145 ret = GNUNET_OK; 146 json_array_foreach (jintervals, index, value) { 147 struct TALER_MERCHANT_StatisticAmountByInterval *jinterval 148 = &resp_intervals[index]; 149 const json_t *amounts_arr; 150 size_t amounts_len; 151 152 struct GNUNET_JSON_Specification spec[] = { 153 GNUNET_JSON_spec_timestamp ("start_time", 154 &jinterval->start_time), 155 GNUNET_JSON_spec_array_const ("cumulative_amounts", 156 &amounts_arr), 157 GNUNET_JSON_spec_end () 158 }; 159 160 if (GNUNET_OK != 161 GNUNET_JSON_parse (value, 162 spec, 163 NULL, NULL)) 164 { 165 GNUNET_break_op (0); 166 ret = GNUNET_SYSERR; 167 continue; 168 } 169 if (GNUNET_SYSERR == ret) 170 break; 171 amounts_len = json_array_size (amounts_arr); 172 { 173 struct TALER_Amount amt_arr[amounts_len]; 174 size_t aindex; 175 json_t *avalue; 176 177 jinterval->cumulative_amount_len = amounts_len; 178 jinterval->cumulative_amounts = amt_arr; 179 json_array_foreach (amounts_arr, aindex, avalue) { 180 if (! json_is_string (avalue)) 181 { 182 GNUNET_break_op (0); 183 return GNUNET_SYSERR; 184 } 185 if (GNUNET_OK != 186 TALER_string_to_amount (json_string_value (avalue), 187 &amt_arr[aindex])) 188 { 189 GNUNET_break_op (0); 190 return GNUNET_SYSERR; 191 } 192 } 193 } 194 } 195 ret = GNUNET_OK; 196 json_array_foreach (jbuckets, index, value) { 197 struct TALER_MERCHANT_StatisticAmountByBucket *jbucket 198 = &resp_buckets[index]; 199 const json_t *amounts_arr; 200 size_t amounts_len; 201 struct GNUNET_JSON_Specification spec[] = { 202 GNUNET_JSON_spec_timestamp ("start_time", 203 &jbucket->start_time), 204 GNUNET_JSON_spec_timestamp ("end_time", 205 &jbucket->end_time), 206 GNUNET_JSON_spec_string ("range", 207 &jbucket->range), 208 GNUNET_JSON_spec_array_const ("cumulative_amounts", 209 &amounts_arr), 210 GNUNET_JSON_spec_end () 211 }; 212 213 if (GNUNET_OK != 214 GNUNET_JSON_parse (value, 215 spec, 216 NULL, NULL)) 217 { 218 GNUNET_break_op (0); 219 ret = GNUNET_SYSERR; 220 continue; 221 } 222 if (GNUNET_SYSERR == ret) 223 break; 224 amounts_len = json_array_size (amounts_arr); 225 if (0 > amounts_len) 226 { 227 GNUNET_break_op (0); 228 ret = GNUNET_SYSERR; 229 break; 230 } 231 { 232 struct TALER_Amount amt_arr[amounts_len]; 233 size_t aindex; 234 json_t *avalue; 235 jbucket->cumulative_amount_len = amounts_len; 236 jbucket->cumulative_amounts = amt_arr; 237 json_array_foreach (amounts_arr, aindex, avalue) { 238 if (! json_is_string (avalue)) 239 { 240 GNUNET_break_op (0); 241 return GNUNET_SYSERR; 242 } 243 if (GNUNET_OK != 244 TALER_string_to_amount (json_string_value (avalue), 245 &amt_arr[aindex])) 246 { 247 GNUNET_break_op (0); 248 return GNUNET_SYSERR; 249 } 250 } 251 } 252 } 253 if (GNUNET_OK == ret) 254 { 255 struct TALER_MERCHANT_StatisticsAmountGetResponse gsr = { 256 .hr.http_status = MHD_HTTP_OK, 257 .hr.reply = json, 258 .details.ok.buckets_length = resp_buckets_len, 259 .details.ok.buckets = resp_buckets, 260 .details.ok.buckets_description = buckets_description, 261 .details.ok.intervals_length = resp_intervals_len, 262 .details.ok.intervals = resp_intervals, 263 .details.ok.intervals_description = intervals_description, 264 }; 265 sgh->cb (sgh->cb_cls, 266 &gsr); 267 sgh->cb = NULL; /* just to be sure */ 268 } 269 return ret; 270 } 271 } 272 273 274 /** 275 * Function called when we're done processing the 276 * HTTP GET /statistics-amount/$SLUG request. 277 * 278 * @param cls the `struct TALER_MERCHANT_StatisticsAmountGetHandle` 279 * @param response_code HTTP response code, 0 on error 280 * @param response response body, NULL if not in JSON 281 */ 282 static void 283 handle_get_statistics_amount_finished (void *cls, 284 long response_code, 285 const void *response) 286 { 287 struct TALER_MERCHANT_StatisticsAmountGetHandle *handle = cls; 288 const json_t *json = response; 289 struct TALER_MERCHANT_StatisticsAmountGetResponse res = { 290 .hr.http_status = (unsigned int) response_code, 291 .hr.reply = json 292 }; 293 294 handle->job = NULL; 295 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 296 "Got /statistics-amount/$SLUG response with status code %u\n", 297 (unsigned int) response_code); 298 switch (response_code) 299 { 300 case MHD_HTTP_OK: 301 { 302 const json_t *buckets; 303 const json_t *intervals; 304 const char *buckets_description = NULL; 305 const char *intervals_description = NULL; 306 struct GNUNET_JSON_Specification spec[] = { 307 GNUNET_JSON_spec_array_const ("buckets", 308 &buckets), 309 GNUNET_JSON_spec_mark_optional ( 310 GNUNET_JSON_spec_string ("buckets_description", 311 &buckets_description), 312 NULL), 313 GNUNET_JSON_spec_array_const ("intervals", 314 &intervals), 315 GNUNET_JSON_spec_mark_optional ( 316 GNUNET_JSON_spec_string ("intervals_description", 317 &intervals_description), 318 NULL), 319 GNUNET_JSON_spec_end () 320 }; 321 322 if (GNUNET_OK != 323 GNUNET_JSON_parse (json, 324 spec, 325 NULL, NULL)) 326 { 327 res.hr.http_status = 0; 328 res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 329 break; 330 } 331 if (GNUNET_OK == 332 parse_intervals_and_buckets_amt (json, 333 buckets, 334 buckets_description, 335 intervals, 336 intervals_description, 337 handle)) 338 { 339 TALER_MERCHANT_statistic_amount_get_cancel (handle); 340 return; 341 } 342 res.hr.http_status = 0; 343 res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 344 break; 345 } 346 case MHD_HTTP_UNAUTHORIZED: 347 res.hr.ec = TALER_JSON_get_error_code (json); 348 res.hr.hint = TALER_JSON_get_error_hint (json); 349 /* Nothing really to verify, merchant says we need to authenticate. */ 350 break; 351 case MHD_HTTP_NOT_FOUND: 352 res.hr.ec = TALER_JSON_get_error_code (json); 353 res.hr.hint = TALER_JSON_get_error_hint (json); 354 break; 355 default: 356 /* unexpected response code */ 357 res.hr.ec = TALER_JSON_get_error_code (json); 358 res.hr.hint = TALER_JSON_get_error_hint (json); 359 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 360 "Unexpected response code %u/%d\n", 361 (unsigned int) response_code, 362 (int) res.hr.ec); 363 break; 364 } 365 } 366 367 368 /** 369 * Parse interval information from @a ia. 370 * 371 * @param json overall JSON reply 372 * @param jbuckets JSON array (or NULL!) with bucket data 373 * @param buckets_description human-readable description for the buckets 374 * @param jintervals JSON array (or NULL!) with bucket data 375 * @param intervals_description human-readable description for the intervals 376 * @param scgh operation handle 377 * @return #GNUNET_OK on success 378 */ 379 static enum GNUNET_GenericReturnValue 380 parse_intervals_and_buckets ( 381 const json_t *json, 382 const json_t *jbuckets, 383 const char *buckets_description, 384 const json_t *jintervals, 385 const char *intervals_description, 386 struct TALER_MERCHANT_StatisticsCounterGetHandle *scgh) 387 { 388 unsigned int resp_buckets_len = json_array_size (jbuckets); 389 unsigned int resp_intervals_len = json_array_size (jintervals); 390 391 if ( (json_array_size (jbuckets) != (size_t) resp_buckets_len) || 392 (json_array_size (jintervals) != (size_t) resp_intervals_len) || 393 (resp_intervals_len = resp_buckets_len > MAX_STATISTICS) ) 394 { 395 GNUNET_break (0); 396 return GNUNET_SYSERR; 397 } 398 { 399 struct TALER_MERCHANT_StatisticCounterByBucket resp_buckets[ 400 GNUNET_NZL (resp_buckets_len)]; 401 struct TALER_MERCHANT_StatisticCounterByInterval resp_intervals[ 402 GNUNET_NZL (resp_intervals_len)]; 403 size_t index; 404 json_t *value; 405 enum GNUNET_GenericReturnValue ret; 406 407 ret = GNUNET_OK; 408 json_array_foreach (jintervals, index, value) { 409 struct TALER_MERCHANT_StatisticCounterByInterval *jinterval 410 = &resp_intervals[index]; 411 struct GNUNET_JSON_Specification spec[] = { 412 GNUNET_JSON_spec_timestamp ("start_time", 413 &jinterval->start_time), 414 GNUNET_JSON_spec_uint64 ("cumulative_counter", 415 &jinterval->cumulative_counter), 416 GNUNET_JSON_spec_end () 417 }; 418 419 if (GNUNET_OK != 420 GNUNET_JSON_parse (value, 421 spec, 422 NULL, NULL)) 423 { 424 GNUNET_break_op (0); 425 ret = GNUNET_SYSERR; 426 continue; 427 } 428 if (GNUNET_SYSERR == ret) 429 break; 430 } 431 ret = GNUNET_OK; 432 json_array_foreach (jbuckets, index, value) { 433 struct TALER_MERCHANT_StatisticCounterByBucket *jbucket = &resp_buckets[ 434 index]; 435 struct GNUNET_JSON_Specification spec[] = { 436 GNUNET_JSON_spec_timestamp ("start_time", 437 &jbucket->start_time), 438 GNUNET_JSON_spec_timestamp ("end_time", 439 &jbucket->end_time), 440 GNUNET_JSON_spec_string ("range", 441 &jbucket->range), 442 GNUNET_JSON_spec_uint64 ("cumulative_counter", 443 &jbucket->cumulative_counter), 444 GNUNET_JSON_spec_end () 445 }; 446 447 if (GNUNET_OK != 448 GNUNET_JSON_parse (value, 449 spec, 450 NULL, NULL)) 451 { 452 GNUNET_break_op (0); 453 ret = GNUNET_SYSERR; 454 continue; 455 } 456 if (GNUNET_SYSERR == ret) 457 break; 458 } 459 if (GNUNET_OK == ret) 460 { 461 struct TALER_MERCHANT_StatisticsCounterGetResponse gsr = { 462 .hr.http_status = MHD_HTTP_OK, 463 .hr.reply = json, 464 .details.ok.buckets_length = resp_buckets_len, 465 .details.ok.buckets = resp_buckets, 466 .details.ok.buckets_description = buckets_description, 467 .details.ok.intervals_length = resp_intervals_len, 468 .details.ok.intervals = resp_intervals, 469 .details.ok.intervals_description = intervals_description, 470 }; 471 scgh->cb (scgh->cb_cls, 472 &gsr); 473 scgh->cb = NULL; /* just to be sure */ 474 } 475 return ret; 476 } 477 } 478 479 480 /** 481 * Function called when we're done processing the 482 * HTTP GET /statistics-counter/$SLUG request. 483 * 484 * @param cls the `struct TALER_MERCHANT_StatisticsCounterGetHandle` 485 * @param response_code HTTP response code, 0 on error 486 * @param response response body, NULL if not in JSON 487 */ 488 static void 489 handle_get_statistics_counter_finished (void *cls, 490 long response_code, 491 const void *response) 492 { 493 struct TALER_MERCHANT_StatisticsCounterGetHandle *handle = cls; 494 const json_t *json = response; 495 struct TALER_MERCHANT_StatisticsCounterGetResponse res = { 496 .hr.http_status = (unsigned int) response_code, 497 .hr.reply = json 498 }; 499 500 handle->job = NULL; 501 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 502 "Got /statistics-counter/$SLUG response with status code %u\n", 503 (unsigned int) response_code); 504 switch (response_code) 505 { 506 case MHD_HTTP_OK: 507 { 508 const json_t *buckets; 509 const json_t *intervals; 510 const char *buckets_description; 511 const char *intervals_description; 512 struct GNUNET_JSON_Specification spec[] = { 513 GNUNET_JSON_spec_array_const ("buckets", 514 &buckets), 515 GNUNET_JSON_spec_mark_optional ( 516 GNUNET_JSON_spec_string ("buckets_description", 517 &buckets_description), 518 NULL), 519 GNUNET_JSON_spec_array_const ("intervals", 520 &intervals), 521 GNUNET_JSON_spec_mark_optional ( 522 GNUNET_JSON_spec_string ("intervals_description", 523 &intervals_description), 524 NULL), 525 GNUNET_JSON_spec_end () 526 }; 527 528 if (GNUNET_OK != 529 GNUNET_JSON_parse (json, 530 spec, 531 NULL, NULL)) 532 { 533 res.hr.http_status = 0; 534 res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 535 break; 536 } 537 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 538 "%s\n", json_dumps (json, JSON_INDENT (1))); 539 if (GNUNET_OK == 540 parse_intervals_and_buckets (json, 541 buckets, 542 buckets_description, 543 intervals, 544 intervals_description, 545 handle)) 546 { 547 TALER_MERCHANT_statistic_counter_get_cancel (handle); 548 return; 549 } 550 res.hr.http_status = 0; 551 res.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 552 break; 553 } 554 case MHD_HTTP_UNAUTHORIZED: 555 res.hr.ec = TALER_JSON_get_error_code (json); 556 res.hr.hint = TALER_JSON_get_error_hint (json); 557 /* Nothing really to verify, merchant says we need to authenticate. */ 558 break; 559 case MHD_HTTP_NOT_FOUND: 560 res.hr.ec = TALER_JSON_get_error_code (json); 561 res.hr.hint = TALER_JSON_get_error_hint (json); 562 break; 563 default: 564 /* unexpected response code */ 565 res.hr.ec = TALER_JSON_get_error_code (json); 566 res.hr.hint = TALER_JSON_get_error_hint (json); 567 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 568 "Unexpected response code %u/%d\n", 569 (unsigned int) response_code, 570 (int) res.hr.ec); 571 break; 572 } 573 } 574 575 576 struct TALER_MERCHANT_StatisticsCounterGetHandle * 577 TALER_MERCHANT_statistic_counter_get ( 578 struct GNUNET_CURL_Context *ctx, 579 const char *backend_url, 580 const char *slug, 581 enum TALER_MERCHANT_StatisticsType stype, 582 TALER_MERCHANT_StatisticsCounterGetCallback cb, 583 void *cb_cls) 584 { 585 struct TALER_MERCHANT_StatisticsCounterGetHandle *handle; 586 CURL *eh; 587 588 handle = GNUNET_new (struct TALER_MERCHANT_StatisticsCounterGetHandle); 589 handle->ctx = ctx; 590 handle->cb = cb; 591 handle->cb_cls = cb_cls; 592 { 593 const char *filter = NULL; 594 char *path; 595 596 switch (stype) 597 { 598 case TALER_MERCHANT_STATISTICS_BY_BUCKET: 599 filter = "bucket"; 600 break; 601 case TALER_MERCHANT_STATISTICS_BY_INTERVAL: 602 filter = "interval"; 603 break; 604 case TALER_MERCHANT_STATISTICS_ALL: 605 filter = NULL; 606 break; 607 } 608 GNUNET_asprintf (&path, 609 "private/statistics-counter/%s", 610 slug); 611 handle->url = TALER_url_join (backend_url, 612 path, 613 "by", 614 filter, 615 NULL); 616 GNUNET_free (path); 617 } 618 if (NULL == handle->url) 619 { 620 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 621 "Could not construct request URL.\n"); 622 GNUNET_free (handle); 623 return NULL; 624 } 625 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 626 "Requesting URL '%s'\n", 627 handle->url); 628 eh = TALER_MERCHANT_curl_easy_get_ (handle->url); 629 handle->job = GNUNET_CURL_job_add (ctx, 630 eh, 631 &handle_get_statistics_counter_finished, 632 handle); 633 return handle; 634 } 635 636 637 void 638 TALER_MERCHANT_statistic_counter_get_cancel ( 639 struct TALER_MERCHANT_StatisticsCounterGetHandle *handle) 640 { 641 if (NULL != handle->job) 642 GNUNET_CURL_job_cancel (handle->job); 643 GNUNET_free (handle->url); 644 GNUNET_free (handle); 645 } 646 647 648 struct TALER_MERCHANT_StatisticsAmountGetHandle * 649 TALER_MERCHANT_statistic_amount_get ( 650 struct GNUNET_CURL_Context *ctx, 651 const char *backend_url, 652 const char *slug, 653 enum TALER_MERCHANT_StatisticsType stype, 654 TALER_MERCHANT_StatisticsAmountGetCallback cb, 655 void *cb_cls) 656 { 657 struct TALER_MERCHANT_StatisticsAmountGetHandle *handle; 658 CURL *eh; 659 660 handle = GNUNET_new (struct TALER_MERCHANT_StatisticsAmountGetHandle); 661 handle->ctx = ctx; 662 handle->cb = cb; 663 handle->cb_cls = cb_cls; 664 { 665 const char *filter = NULL; 666 char *path; 667 668 switch (stype) 669 { 670 case TALER_MERCHANT_STATISTICS_BY_BUCKET: 671 filter = "bucket"; 672 break; 673 case TALER_MERCHANT_STATISTICS_BY_INTERVAL: 674 filter = "interval"; 675 break; 676 case TALER_MERCHANT_STATISTICS_ALL: 677 filter = NULL; 678 break; 679 } 680 GNUNET_asprintf (&path, 681 "private/statistics-amount/%s", 682 slug); 683 handle->url = TALER_url_join (backend_url, 684 path, 685 "by", 686 filter, 687 NULL); 688 GNUNET_free (path); 689 } 690 if (NULL == handle->url) 691 { 692 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 693 "Could not construct request URL.\n"); 694 GNUNET_free (handle); 695 return NULL; 696 } 697 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 698 "Requesting URL '%s'\n", 699 handle->url); 700 eh = TALER_MERCHANT_curl_easy_get_ (handle->url); 701 handle->job = GNUNET_CURL_job_add (ctx, 702 eh, 703 &handle_get_statistics_amount_finished, 704 handle); 705 return handle; 706 } 707 708 709 void 710 TALER_MERCHANT_statistic_amount_get_cancel ( 711 struct TALER_MERCHANT_StatisticsAmountGetHandle *handle) 712 { 713 if (NULL != handle->job) 714 GNUNET_CURL_job_cancel (handle->job); 715 GNUNET_free (handle->url); 716 GNUNET_free (handle); 717 }