taler-exchange-httpd_aml-accounts-get.c (15838B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 2025, 2026 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 taler-exchange-httpd_aml-accounts-get.c 18 * @brief Return summary information about accounts 19 * @author Christian Grothoff 20 */ 21 #include "taler/platform.h" 22 #include <gnunet/gnunet_util_lib.h> 23 #include <jansson.h> 24 #include <microhttpd.h> 25 #include <pthread.h> 26 #include "taler/taler_json_lib.h" 27 #include "taler/taler_mhd_lib.h" 28 #include "taler/taler_signatures.h" 29 #include "taler-exchange-httpd.h" 30 #include "taler/taler_exchangedb_plugin.h" 31 #include "taler-exchange-httpd_aml-accounts-get.h" 32 33 34 /** 35 * Maximum number of records we return in one go. Must be 36 * small enough to ensure that XML/CSV encoded results stay 37 * below the 16MB limit of GNUNET_malloc(). 38 */ 39 #define MAX_RECORDS (1024 * 64) 40 41 #define CSV_HEADER \ 42 "File number,Customer,Comments,Risky,Acquisition date,Exit date\r\n" 43 #define CSV_FOOTER "\r\n" 44 45 #define XML_HEADER "<?xml version=\"1.0\" encoding=\"UTF 8\"?>" \ 46 "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \ 47 " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \ 48 " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \ 49 " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \ 50 " xmlns:o=\"urn:schemas-microsoft-com:office:office\"" \ 51 " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"" \ 52 " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \ 53 "<Styles><Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd\"/></Style></Styles>\n" \ 54 "<Worksheet ss:Name=\"Accounts\">\n" \ 55 "<Table>\n" \ 56 "<Row>\n" \ 57 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">File number</Data></Cell>\n" \ 58 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Customer</Data></Cell>\n" \ 59 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Comments</Data></Cell>\n" \ 60 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Increased risk business relationship</Data></Cell>\n" \ 61 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Acquisition date</Data></Cell>\n" \ 62 "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Exit date</Data></Cell>\n" \ 63 "</Row>\n" 64 #define XML_FOOTER "</Table></Worksheet></Workbook>" 65 66 /** 67 * Closure for the record_cb(). 68 */ 69 struct ResponseContext 70 { 71 /** 72 * Format of the response we are to generate. 73 */ 74 enum 75 { 76 RCF_JSON, 77 RCF_XML, 78 RCF_CSV 79 } format; 80 81 /** 82 * Where we store the response data. 83 */ 84 union 85 { 86 /** 87 * If @e format is #RCF_JSON. 88 */ 89 json_t *json; 90 91 /** 92 * If @e format is #RCF_XML. 93 */ 94 struct GNUNET_Buffer xml; 95 96 /** 97 * If @e format is #RCF_CSV. 98 */ 99 struct GNUNET_Buffer csv; 100 101 } details; 102 }; 103 104 105 /** 106 * Free resources from @a rc 107 * 108 * @param[in] rc context to clean up 109 */ 110 static void 111 free_rc (struct ResponseContext *rc) 112 { 113 switch (rc->format) 114 { 115 case RCF_JSON: 116 json_decref (rc->details.json); 117 break; 118 case RCF_XML: 119 GNUNET_buffer_clear (&rc->details.xml); 120 break; 121 case RCF_CSV: 122 GNUNET_buffer_clear (&rc->details.csv); 123 break; 124 } 125 } 126 127 128 /** 129 * Return account summary information. 130 * 131 * @param cls closure 132 * @param row_id current row in AML status table 133 * @param h_payto account for which the attribute data is stored 134 * @param open_time when was the account opened formally, 135 * GNUNET_TIME_UNIT_FOREVER_TS if it was never opened 136 * @param close_time when was the account formally closed, 137 * GNUNET_TIME_UNIT_ZERO_TS if it was never closed 138 * @param comments comments on the account 139 * @param high_risk is this a high-risk business relationship 140 * @param to_investigate TRUE if this account should be investigated 141 * @param payto the payto URI of the account 142 */ 143 static void 144 record_cb ( 145 void *cls, 146 uint64_t row_id, 147 const struct TALER_NormalizedPaytoHashP *h_payto, 148 struct GNUNET_TIME_Timestamp open_time, 149 struct GNUNET_TIME_Timestamp close_time, 150 const char *comments, 151 bool high_risk, 152 bool to_investigate, 153 struct TALER_FullPayto payto) 154 { 155 struct ResponseContext *rc = cls; 156 157 if ( (NULL == comments) && 158 (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) 159 comments = "transacted amounts below limits that trigger account opening"; 160 if (NULL == comments) 161 comments = ""; 162 switch (rc->format) 163 { 164 case RCF_JSON: 165 GNUNET_assert ( 166 0 == 167 json_array_append_new ( 168 rc->details.json, 169 GNUNET_JSON_PACK ( 170 GNUNET_JSON_pack_data_auto ("h_payto", 171 h_payto), 172 TALER_JSON_pack_full_payto ("full_payto", 173 payto), 174 GNUNET_JSON_pack_bool ("high_risk", 175 high_risk), 176 GNUNET_JSON_pack_allow_null ( 177 GNUNET_JSON_pack_string ("comments", 178 comments)), 179 GNUNET_JSON_pack_int64 ("rowid", 180 row_id), 181 GNUNET_JSON_pack_timestamp ("open_time", 182 open_time), 183 GNUNET_JSON_pack_timestamp ("close_time", 184 close_time), 185 GNUNET_JSON_pack_bool ("to_investigate", 186 to_investigate) 187 ))); 188 return; 189 case RCF_XML: 190 { 191 char *epayto; 192 char *ecomments = NULL; 193 char opentime_s[128]; 194 char closetime_s[128]; 195 const struct tm *tm; 196 time_t tt; 197 198 epayto = TALER_escape_xml (payto.full_payto); 199 if ( (NULL == comments) && 200 (GNUNET_TIME_absolute_is_never (open_time.abs_time)) ) 201 comments = 202 "transacted amounts below limits that trigger account opening"; 203 ecomments = TALER_escape_xml (comments); 204 tt = (time_t) GNUNET_TIME_timestamp_to_s (open_time); 205 tm = gmtime (&tt); 206 strftime (opentime_s, 207 sizeof (opentime_s), 208 "%Y-%m-%dT%H:%M:%S", 209 tm); 210 tt = (time_t) GNUNET_TIME_timestamp_to_s (close_time); 211 tm = gmtime (&tt); 212 strftime (closetime_s, 213 sizeof (closetime_s), 214 "%Y-%m-%dT%H:%M:%S", 215 tm); 216 GNUNET_buffer_write_fstr ( 217 &rc->details.xml, 218 "<Row>" 219 "<Cell><Data ss:Type=\"Number\">%llu</Data></Cell>" 220 "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" 221 "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" 222 "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" 223 "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" 224 "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>" 225 "</Row>\n", 226 (unsigned long long) row_id, 227 epayto, 228 NULL == ecomments 229 ? "" 230 : ecomments, 231 high_risk ? "TRUE" : "FALSE", 232 high_risk ? "1" : "0", 233 GNUNET_TIME_absolute_is_never (open_time.abs_time) 234 ? "String" 235 : "DateTime", 236 GNUNET_TIME_absolute_is_never (open_time.abs_time) 237 ? "never" 238 : opentime_s, 239 GNUNET_TIME_absolute_is_never (close_time.abs_time) 240 ? "String" 241 : "DateTime", 242 GNUNET_TIME_absolute_is_never (close_time.abs_time) 243 ? "never" 244 : closetime_s); 245 GNUNET_free (ecomments); 246 GNUNET_free (epayto); 247 break; 248 } /* end case RCF_XML */ 249 case RCF_CSV: 250 { 251 char *ecomments; 252 char otbuf[64]; 253 char ctbuf[64]; 254 size_t len = strlen (comments); 255 size_t wpos = 0; 256 257 GNUNET_snprintf (otbuf, 258 sizeof (otbuf), 259 "%s", 260 GNUNET_TIME_timestamp2s (open_time)); 261 GNUNET_snprintf (ctbuf, 262 sizeof (ctbuf), 263 "%s", 264 GNUNET_TIME_timestamp2s (close_time)); 265 /* Escape 'comments' to double '"' as per RFC 4180, 2.7. */ 266 ecomments = GNUNET_malloc (2 * len + 1); 267 for (size_t off = 0; off<len; off++) 268 { 269 if ('"' == comments[off]) 270 ecomments[wpos++] = '"'; 271 ecomments[wpos++] = comments[off]; 272 } 273 GNUNET_buffer_write_fstr (&rc->details.csv, 274 "%llu,\"%s\",\"%s\",%s,%s,%s\r\n", 275 (unsigned long long) row_id, 276 payto.full_payto, 277 ecomments, 278 high_risk ? "X":" ", 279 GNUNET_TIME_absolute_is_never (open_time. 280 abs_time) 281 ? "-" 282 : otbuf, 283 GNUNET_TIME_absolute_is_never (close_time. 284 abs_time) 285 ? "-" 286 : ctbuf); 287 GNUNET_free (ecomments); 288 break; 289 } /* end case RCF_CSV */ 290 } /* end switch */ 291 } 292 293 294 MHD_RESULT 295 TEH_handler_aml_accounts_get ( 296 struct TEH_RequestContext *rc, 297 const struct TALER_AmlOfficerPublicKeyP *officer_pub, 298 const char *const args[]) 299 { 300 struct ResponseContext rctx; 301 int64_t limit = -20; 302 uint64_t offset; 303 enum TALER_EXCHANGE_YesNoAll open_filter; 304 enum TALER_EXCHANGE_YesNoAll high_risk_filter; 305 enum TALER_EXCHANGE_YesNoAll investigation_filter; 306 307 memset (&rctx, 308 0, 309 sizeof (rctx)); 310 if (NULL != args[0]) 311 { 312 GNUNET_break_op (0); 313 return TALER_MHD_reply_with_error ( 314 rc->connection, 315 MHD_HTTP_NOT_FOUND, 316 TALER_EC_GENERIC_ENDPOINT_UNKNOWN, 317 args[0]); 318 } 319 TALER_MHD_parse_request_snumber (rc->connection, 320 "limit", 321 &limit); 322 if (limit > 0) 323 offset = 0; 324 else 325 offset = INT64_MAX; 326 TALER_MHD_parse_request_number (rc->connection, 327 "offset", 328 &offset); 329 if (offset > INT64_MAX) 330 { 331 GNUNET_break_op (0); /* broken client */ 332 offset = INT64_MAX; 333 } 334 TALER_MHD_parse_request_yna (rc->connection, 335 "open", 336 TALER_EXCHANGE_YNA_ALL, 337 &open_filter); 338 TALER_MHD_parse_request_yna (rc->connection, 339 "investigation", 340 TALER_EXCHANGE_YNA_ALL, 341 &investigation_filter); 342 TALER_MHD_parse_request_yna (rc->connection, 343 "high_risk", 344 TALER_EXCHANGE_YNA_ALL, 345 &high_risk_filter); 346 { 347 const char *mime; 348 349 mime = MHD_lookup_connection_value (rc->connection, 350 MHD_HEADER_KIND, 351 MHD_HTTP_HEADER_ACCEPT); 352 if (NULL == mime) 353 mime = "application/json"; 354 if (0 == strcmp (mime, 355 "application/json")) 356 { 357 rctx.format = RCF_JSON; 358 rctx.details.json = json_array (); 359 GNUNET_assert (NULL != rctx.details.json); 360 } 361 else if (0 == strcmp (mime, 362 "application/vnd.ms-excel")) 363 { 364 rctx.format = RCF_XML; 365 GNUNET_buffer_write_str (&rctx.details.xml, 366 XML_HEADER); 367 } 368 else if (0 == strcmp (mime, 369 "text/csv")) 370 { 371 rctx.format = RCF_CSV; 372 GNUNET_buffer_write_str (&rctx.details.csv, 373 CSV_HEADER); 374 } 375 else 376 { 377 GNUNET_break_op (0); 378 return TALER_MHD_reply_with_error ( 379 rc->connection, 380 MHD_HTTP_NOT_ACCEPTABLE, 381 TALER_EC_GENERIC_PARAMETER_MALFORMED, 382 mime); 383 } 384 } 385 386 { 387 enum GNUNET_DB_QueryStatus qs; 388 389 if (limit > MAX_RECORDS) 390 limit = MAX_RECORDS; 391 if (limit < -MAX_RECORDS) 392 limit = -MAX_RECORDS; 393 qs = TEH_plugin->select_kyc_accounts ( 394 TEH_plugin->cls, 395 investigation_filter, 396 open_filter, 397 high_risk_filter, 398 offset, 399 limit, 400 &record_cb, 401 &rctx); 402 switch (qs) 403 { 404 case GNUNET_DB_STATUS_HARD_ERROR: 405 case GNUNET_DB_STATUS_SOFT_ERROR: 406 free_rc (&rctx); 407 GNUNET_break (0); 408 return TALER_MHD_reply_with_error ( 409 rc->connection, 410 MHD_HTTP_INTERNAL_SERVER_ERROR, 411 TALER_EC_GENERIC_DB_FETCH_FAILED, 412 "select_kyx_accounts"); 413 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 414 free_rc (&rctx); 415 return TALER_MHD_reply_static ( 416 rc->connection, 417 MHD_HTTP_NO_CONTENT, 418 NULL, 419 NULL, 420 0); 421 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 422 break; 423 } /* end switch (qs) */ 424 425 switch (rctx.format) 426 { 427 case RCF_JSON: 428 return TALER_MHD_REPLY_JSON_PACK ( 429 rc->connection, 430 MHD_HTTP_OK, 431 GNUNET_JSON_pack_array_steal ("accounts", 432 rctx.details.json)); 433 case RCF_XML: 434 { 435 struct MHD_Response *resp; 436 MHD_RESULT mret; 437 438 GNUNET_buffer_write_str (&rctx.details.xml, 439 XML_FOOTER); 440 /* FIXME: add support for compression */ 441 resp = MHD_create_response_from_buffer (rctx.details.xml.position, 442 rctx.details.xml.mem, 443 MHD_RESPMEM_MUST_FREE); 444 TALER_MHD_add_global_headers (resp, 445 false); 446 GNUNET_break (MHD_YES == 447 MHD_add_response_header (resp, 448 MHD_HTTP_HEADER_CONTENT_TYPE, 449 "application/vnd.ms-excel")); 450 mret = MHD_queue_response (rc->connection, 451 MHD_HTTP_OK, 452 resp); 453 MHD_destroy_response (resp); 454 return mret; 455 } 456 case RCF_CSV: 457 { 458 struct MHD_Response *resp; 459 MHD_RESULT mret; 460 461 GNUNET_buffer_write_str (&rctx.details.csv, 462 CSV_FOOTER); 463 /* FIXME: add support for compression */ 464 resp = MHD_create_response_from_buffer (rctx.details.csv.position, 465 rctx.details.csv.mem, 466 MHD_RESPMEM_MUST_FREE); 467 TALER_MHD_add_global_headers (resp, 468 false); 469 GNUNET_break (MHD_YES == 470 MHD_add_response_header (resp, 471 MHD_HTTP_HEADER_CONTENT_TYPE, 472 "text/csv")); 473 mret = MHD_queue_response (rc->connection, 474 MHD_HTTP_OK, 475 resp); 476 MHD_destroy_response (resp); 477 return mret; 478 } 479 } /* end switch (rctx.format) */ 480 } 481 GNUNET_break (0); 482 return MHD_NO; 483 } 484 485 486 /* end of taler-exchange-httpd_aml-accounts_get.c */