taler-exchange-httpd_purses_deposit.c (16682B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022 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_purses_deposit.c 18 * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and 19 * verifies the coin signature before handing things off 20 * to the database. 21 * @author Christian Grothoff 22 */ 23 #include "taler/platform.h" 24 #include <gnunet/gnunet_util_lib.h> 25 #include <gnunet/gnunet_json_lib.h> 26 #include <jansson.h> 27 #include <microhttpd.h> 28 #include "taler/taler_dbevents.h" 29 #include "taler/taler_json_lib.h" 30 #include "taler/taler_mhd_lib.h" 31 #include "taler-exchange-httpd_common_deposit.h" 32 #include "taler-exchange-httpd_purses_deposit.h" 33 #include "taler-exchange-httpd_responses.h" 34 #include "taler/taler_exchangedb_lib.h" 35 #include "taler-exchange-httpd_keys.h" 36 37 38 /** 39 * Closure for #deposit_transaction. 40 */ 41 struct PurseDepositContext 42 { 43 /** 44 * Public key of the purse we are creating. 45 */ 46 const struct TALER_PurseContractPublicKeyP *purse_pub; 47 48 /** 49 * Total amount to be put into the purse. 50 */ 51 struct TALER_Amount amount; 52 53 /** 54 * Total actually deposited by all the coins. 55 */ 56 struct TALER_Amount deposit_total; 57 58 /** 59 * When should the purse expire. 60 */ 61 struct GNUNET_TIME_Timestamp purse_expiration; 62 63 /** 64 * Hash of the contract (needed for signing). 65 */ 66 struct TALER_PrivateContractHashP h_contract_terms; 67 68 /** 69 * Our current time. 70 */ 71 struct GNUNET_TIME_Timestamp exchange_timestamp; 72 73 /** 74 * Array of coins being deposited. 75 */ 76 struct TEH_PurseDepositedCoin *coins; 77 78 /** 79 * Length of the @e coins array. 80 */ 81 unsigned int num_coins; 82 83 /** 84 * Minimum age for deposits into this purse. 85 */ 86 uint32_t min_age; 87 }; 88 89 90 /** 91 * Send confirmation of purse creation success to client. 92 * 93 * @param connection connection to the client 94 * @param pcc details about the request that succeeded 95 * @return MHD result code 96 */ 97 static MHD_RESULT 98 reply_deposit_success (struct MHD_Connection *connection, 99 const struct PurseDepositContext *pcc) 100 { 101 struct TALER_ExchangePublicKeyP pub; 102 struct TALER_ExchangeSignatureP sig; 103 enum TALER_ErrorCode ec; 104 105 if (TALER_EC_NONE != 106 (ec = TALER_exchange_online_purse_created_sign ( 107 &TEH_keys_exchange_sign_, 108 pcc->exchange_timestamp, 109 pcc->purse_expiration, 110 &pcc->amount, 111 &pcc->deposit_total, 112 pcc->purse_pub, 113 &pcc->h_contract_terms, 114 &pub, 115 &sig))) 116 { 117 GNUNET_break (0); 118 return TALER_MHD_reply_with_ec (connection, 119 ec, 120 NULL); 121 } 122 return TALER_MHD_REPLY_JSON_PACK ( 123 connection, 124 MHD_HTTP_OK, 125 TALER_JSON_pack_amount ("total_deposited", 126 &pcc->deposit_total), 127 TALER_JSON_pack_amount ("purse_value_after_fees", 128 &pcc->amount), 129 GNUNET_JSON_pack_timestamp ("exchange_timestamp", 130 pcc->exchange_timestamp), 131 GNUNET_JSON_pack_timestamp ("purse_expiration", 132 pcc->purse_expiration), 133 GNUNET_JSON_pack_data_auto ("h_contract_terms", 134 &pcc->h_contract_terms), 135 GNUNET_JSON_pack_data_auto ("exchange_sig", 136 &sig), 137 GNUNET_JSON_pack_data_auto ("exchange_pub", 138 &pub)); 139 } 140 141 142 /** 143 * Execute database transaction for /purses/$PID/deposit. Runs the transaction 144 * logic; IF it returns a non-error code, the transaction logic MUST NOT queue 145 * a MHD response. IF it returns an hard error, the transaction logic MUST 146 * queue a MHD response and set @a mhd_ret. IF it returns the soft error 147 * code, the function MAY be called again to retry and MUST not queue a MHD 148 * response. 149 * 150 * @param cls a `struct PurseDepositContext` 151 * @param connection MHD request context 152 * @param[out] mhd_ret set to MHD status on error 153 * @return transaction status 154 */ 155 static enum GNUNET_DB_QueryStatus 156 deposit_transaction (void *cls, 157 struct MHD_Connection *connection, 158 MHD_RESULT *mhd_ret) 159 { 160 struct PurseDepositContext *pcc = cls; 161 enum GNUNET_DB_QueryStatus qs; 162 163 qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; 164 for (unsigned int i = 0; i<pcc->num_coins; i++) 165 { 166 struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; 167 bool balance_ok = false; 168 bool conflict = true; 169 bool too_late = true; 170 171 qs = TEH_make_coin_known (&coin->cpi, 172 connection, 173 &coin->known_coin_id, 174 mhd_ret); 175 if (qs < 0) 176 return qs; 177 qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls, 178 pcc->purse_pub, 179 &coin->cpi.coin_pub, 180 &coin->amount, 181 &coin->coin_sig, 182 &coin->amount_minus_fee, 183 &balance_ok, 184 &too_late, 185 &conflict); 186 if (qs <= 0) 187 { 188 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 189 return qs; 190 GNUNET_break (0 != qs); 191 TALER_LOG_WARNING ( 192 "Failed to store purse deposit information in database\n"); 193 *mhd_ret = TALER_MHD_reply_with_error (connection, 194 MHD_HTTP_INTERNAL_SERVER_ERROR, 195 TALER_EC_GENERIC_DB_STORE_FAILED, 196 "do purse deposit"); 197 return GNUNET_DB_STATUS_HARD_ERROR; 198 } 199 if (! balance_ok) 200 { 201 *mhd_ret 202 = TEH_RESPONSE_reply_coin_insufficient_funds ( 203 connection, 204 TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, 205 &coin->cpi.denom_pub_hash, 206 &coin->cpi.coin_pub); 207 return GNUNET_DB_STATUS_HARD_ERROR; 208 } 209 if (too_late) 210 { 211 TEH_plugin->rollback (TEH_plugin->cls); 212 *mhd_ret 213 = TALER_MHD_reply_with_ec ( 214 connection, 215 TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY, 216 NULL); 217 return GNUNET_DB_STATUS_HARD_ERROR; 218 } 219 if (conflict) 220 { 221 struct TALER_Amount amount; 222 struct TALER_CoinSpendPublicKeyP coin_pub; 223 struct TALER_CoinSpendSignatureP coin_sig; 224 struct TALER_DenominationHashP h_denom_pub; 225 struct TALER_AgeCommitmentHashP phac; 226 char *partner_url = NULL; 227 228 TEH_plugin->rollback (TEH_plugin->cls); 229 qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls, 230 pcc->purse_pub, 231 &coin->cpi.coin_pub, 232 &amount, 233 &h_denom_pub, 234 &phac, 235 &coin_sig, 236 &partner_url); 237 if (qs < 0) 238 { 239 GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); 240 TALER_LOG_WARNING ( 241 "Failed to fetch purse deposit information from database\n"); 242 *mhd_ret = TALER_MHD_reply_with_error (connection, 243 MHD_HTTP_INTERNAL_SERVER_ERROR, 244 TALER_EC_GENERIC_DB_FETCH_FAILED, 245 "get purse deposit"); 246 return GNUNET_DB_STATUS_HARD_ERROR; 247 } 248 249 *mhd_ret 250 = TALER_MHD_REPLY_JSON_PACK ( 251 connection, 252 MHD_HTTP_CONFLICT, 253 TALER_JSON_pack_ec ( 254 TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), 255 GNUNET_JSON_pack_data_auto ("coin_pub", 256 &coin_pub), 257 GNUNET_JSON_pack_data_auto ("h_denom_pub", 258 &h_denom_pub), 259 GNUNET_JSON_pack_data_auto ("h_age_commitment", 260 &phac), 261 GNUNET_JSON_pack_data_auto ("coin_sig", 262 &coin_sig), 263 GNUNET_JSON_pack_allow_null ( 264 GNUNET_JSON_pack_string ("partner_url", 265 partner_url)), 266 TALER_JSON_pack_amount ("amount", 267 &amount)); 268 GNUNET_free (partner_url); 269 return GNUNET_DB_STATUS_HARD_ERROR; 270 } 271 } 272 return qs; 273 } 274 275 276 /** 277 * Parse a coin and check signature of the coin and the denomination 278 * signature over the coin. 279 * 280 * @param[in,out] connection our HTTP connection 281 * @param[in,out] pcc request context 282 * @param[out] coin coin to initialize 283 * @param jcoin coin to parse 284 * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, 285 * #GNUNET_SYSERR on failure and no error could be returned 286 */ 287 static enum GNUNET_GenericReturnValue 288 parse_coin (struct MHD_Connection *connection, 289 struct PurseDepositContext *pcc, 290 struct TEH_PurseDepositedCoin *coin, 291 const json_t *jcoin) 292 { 293 enum GNUNET_GenericReturnValue iret; 294 295 if (GNUNET_OK != 296 (iret = TEH_common_purse_deposit_parse_coin (connection, 297 coin, 298 jcoin))) 299 return iret; 300 if (GNUNET_OK != 301 (iret = TEH_common_deposit_check_purse_deposit ( 302 connection, 303 coin, 304 pcc->purse_pub, 305 pcc->min_age))) 306 return iret; 307 if (0 > 308 TALER_amount_add (&pcc->deposit_total, 309 &pcc->deposit_total, 310 &coin->amount_minus_fee)) 311 { 312 GNUNET_break (0); 313 return TALER_MHD_reply_with_error (connection, 314 MHD_HTTP_INTERNAL_SERVER_ERROR, 315 TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, 316 "total deposit contribution"); 317 } 318 return GNUNET_OK; 319 } 320 321 322 MHD_RESULT 323 TEH_handler_purses_deposit ( 324 struct TEH_RequestContext *rc, 325 const struct TALER_PurseContractPublicKeyP *purse_pub, 326 const json_t *root) 327 { 328 struct MHD_Connection *connection = rc->connection; 329 struct PurseDepositContext pcc = { 330 .purse_pub = purse_pub, 331 .exchange_timestamp = GNUNET_TIME_timestamp_get () 332 }; 333 const json_t *deposits; 334 json_t *deposit; 335 unsigned int idx; 336 struct GNUNET_JSON_Specification spec[] = { 337 GNUNET_JSON_spec_array_const ("deposits", 338 &deposits), 339 GNUNET_JSON_spec_end () 340 }; 341 342 { 343 enum GNUNET_GenericReturnValue res; 344 345 res = TALER_MHD_parse_json_data (connection, 346 root, 347 spec); 348 if (GNUNET_SYSERR == res) 349 { 350 GNUNET_break (0); 351 return MHD_NO; /* hard failure */ 352 } 353 if (GNUNET_NO == res) 354 { 355 GNUNET_break_op (0); 356 return MHD_YES; /* failure */ 357 } 358 } 359 GNUNET_assert (GNUNET_OK == 360 TALER_amount_set_zero (TEH_currency, 361 &pcc.deposit_total)); 362 pcc.num_coins = (unsigned int) json_array_size (deposits); 363 if ( (0 == pcc.num_coins) || 364 (((size_t) pcc.num_coins) != json_array_size (deposits)) || 365 (pcc.num_coins > TALER_MAX_COINS) ) 366 { 367 GNUNET_break_op (0); 368 return TALER_MHD_reply_with_error (connection, 369 MHD_HTTP_BAD_REQUEST, 370 TALER_EC_GENERIC_PARAMETER_MALFORMED, 371 "deposits"); 372 } 373 374 { 375 enum GNUNET_DB_QueryStatus qs; 376 struct GNUNET_TIME_Timestamp create_timestamp; 377 struct GNUNET_TIME_Timestamp merge_timestamp; 378 bool was_deleted; 379 bool was_refunded; 380 381 qs = TEH_plugin->select_purse ( 382 TEH_plugin->cls, 383 pcc.purse_pub, 384 &create_timestamp, 385 &pcc.purse_expiration, 386 &pcc.amount, 387 &pcc.deposit_total, 388 &pcc.h_contract_terms, 389 &merge_timestamp, 390 &was_deleted, 391 &was_refunded); 392 switch (qs) 393 { 394 case GNUNET_DB_STATUS_HARD_ERROR: 395 GNUNET_break (0); 396 return TALER_MHD_reply_with_error (connection, 397 MHD_HTTP_INTERNAL_SERVER_ERROR, 398 TALER_EC_GENERIC_DB_FETCH_FAILED, 399 "select purse"); 400 case GNUNET_DB_STATUS_SOFT_ERROR: 401 GNUNET_break (0); 402 return TALER_MHD_reply_with_error (connection, 403 MHD_HTTP_INTERNAL_SERVER_ERROR, 404 TALER_EC_GENERIC_DB_FETCH_FAILED, 405 "select purse"); 406 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 407 return TALER_MHD_reply_with_error (connection, 408 MHD_HTTP_NOT_FOUND, 409 TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, 410 NULL); 411 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 412 break; /* handled below */ 413 } 414 if (was_refunded || 415 was_deleted) 416 { 417 return TALER_MHD_reply_with_error ( 418 connection, 419 MHD_HTTP_GONE, 420 was_deleted 421 ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED 422 : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, 423 GNUNET_TIME_timestamp2s (pcc.purse_expiration)); 424 } 425 } 426 427 /* parse deposits */ 428 pcc.coins = GNUNET_new_array (pcc.num_coins, 429 struct TEH_PurseDepositedCoin); 430 json_array_foreach (deposits, idx, deposit) 431 { 432 enum GNUNET_GenericReturnValue res; 433 struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; 434 435 res = parse_coin (connection, 436 &pcc, 437 coin, 438 deposit); 439 if (GNUNET_OK != res) 440 { 441 for (unsigned int i = 0; i<idx; i++) 442 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 443 GNUNET_free (pcc.coins); 444 return (GNUNET_NO == res) ? MHD_YES : MHD_NO; 445 } 446 } 447 448 if (GNUNET_SYSERR == 449 TEH_plugin->preflight (TEH_plugin->cls)) 450 { 451 GNUNET_break (0); 452 for (unsigned int i = 0; i<pcc.num_coins; i++) 453 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 454 GNUNET_free (pcc.coins); 455 return TALER_MHD_reply_with_error (connection, 456 MHD_HTTP_INTERNAL_SERVER_ERROR, 457 TALER_EC_GENERIC_DB_START_FAILED, 458 "preflight failure"); 459 } 460 461 /* execute transaction */ 462 { 463 MHD_RESULT mhd_ret; 464 465 if (GNUNET_OK != 466 TEH_DB_run_transaction (connection, 467 "execute purse deposit", 468 TEH_MT_REQUEST_PURSE_DEPOSIT, 469 &mhd_ret, 470 &deposit_transaction, 471 &pcc)) 472 { 473 for (unsigned int i = 0; i<pcc.num_coins; i++) 474 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 475 GNUNET_free (pcc.coins); 476 return mhd_ret; 477 } 478 } 479 { 480 struct TALER_PurseEventP rep = { 481 .header.size = htons (sizeof (rep)), 482 .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), 483 .purse_pub = *pcc.purse_pub 484 }; 485 486 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 487 "Notifying about purse deposit %s\n", 488 TALER_B2S (pcc.purse_pub)); 489 TEH_plugin->event_notify (TEH_plugin->cls, 490 &rep.header, 491 NULL, 492 0); 493 } 494 495 /* generate regular response */ 496 { 497 MHD_RESULT res; 498 499 res = reply_deposit_success (connection, 500 &pcc); 501 for (unsigned int i = 0; i<pcc.num_coins; i++) 502 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 503 GNUNET_free (pcc.coins); 504 return res; 505 } 506 } 507 508 509 /* end of taler-exchange-httpd_purses_deposit.c */