taler-exchange-httpd_refund.c (12302B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2014-2020 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_refund.c 18 * @brief Handle refund requests; parses the POST and JSON and 19 * verifies the coin signature before handing things off 20 * to the database. 21 * @author Florian Dold 22 * @author Benedikt Mueller 23 * @author Christian Grothoff 24 */ 25 #include "taler/platform.h" 26 #include <gnunet/gnunet_util_lib.h> 27 #include <gnunet/gnunet_json_lib.h> 28 #include <jansson.h> 29 #include <microhttpd.h> 30 #include <pthread.h> 31 #include "taler/taler_json_lib.h" 32 #include "taler/taler_mhd_lib.h" 33 #include "taler-exchange-httpd_refund.h" 34 #include "taler-exchange-httpd_responses.h" 35 #include "taler-exchange-httpd_keys.h" 36 37 38 /** 39 * How often do we retry after soft database errors? 40 */ 41 #define MAX_RETRIES 3 42 43 44 /** 45 * Generate successful refund confirmation message. 46 * 47 * @param connection connection to the client 48 * @param coin_pub public key of the coin 49 * @param refund details about the successful refund 50 * @return MHD result code 51 */ 52 static MHD_RESULT 53 reply_refund_success (struct MHD_Connection *connection, 54 const struct TALER_CoinSpendPublicKeyP *coin_pub, 55 const struct TALER_EXCHANGEDB_RefundListEntry *refund) 56 { 57 struct TALER_ExchangePublicKeyP pub; 58 struct TALER_ExchangeSignatureP sig; 59 enum TALER_ErrorCode ec; 60 61 if (TALER_EC_NONE != 62 (ec = TALER_exchange_online_refund_confirmation_sign ( 63 &TEH_keys_exchange_sign_, 64 &refund->h_contract_terms, 65 coin_pub, 66 &refund->merchant_pub, 67 refund->rtransaction_id, 68 &refund->refund_amount, 69 &pub, 70 &sig))) 71 { 72 return TALER_MHD_reply_with_ec (connection, 73 ec, 74 NULL); 75 } 76 return TALER_MHD_REPLY_JSON_PACK ( 77 connection, 78 MHD_HTTP_OK, 79 GNUNET_JSON_pack_data_auto ("exchange_sig", 80 &sig), 81 GNUNET_JSON_pack_data_auto ("exchange_pub", 82 &pub)); 83 } 84 85 86 /** 87 * Closure for refund_transaction(). 88 */ 89 struct RefundContext 90 { 91 /** 92 * Details about the deposit operation. 93 */ 94 const struct TALER_EXCHANGEDB_Refund *refund; 95 96 /** 97 * Deposit fee of the coin. 98 */ 99 struct TALER_Amount deposit_fee; 100 101 /** 102 * Unique ID of the coin in known_coins. 103 */ 104 uint64_t known_coin_id; 105 }; 106 107 108 /** 109 * Execute a "/refund" transaction. Returns a confirmation that the 110 * refund was successful, or a failure if we are not aware of a 111 * matching /deposit or if it is too late to do the refund. 112 * 113 * IF it returns a non-error code, the transaction logic MUST 114 * NOT queue a MHD response. IF it returns an hard error, the 115 * transaction logic MUST queue a MHD response and set @a mhd_ret. IF 116 * it returns the soft error code, the function MAY be called again to 117 * retry and MUST not queue a MHD response. 118 * 119 * @param cls closure with a `const struct TALER_EXCHANGEDB_Refund *` 120 * @param connection MHD request which triggered the transaction 121 * @param[out] mhd_ret set to MHD response status for @a connection, 122 * if transaction failed (!) 123 * @return transaction status 124 */ 125 static enum GNUNET_DB_QueryStatus 126 refund_transaction (void *cls, 127 struct MHD_Connection *connection, 128 MHD_RESULT *mhd_ret) 129 { 130 struct RefundContext *rctx = cls; 131 const struct TALER_EXCHANGEDB_Refund *refund = rctx->refund; 132 enum GNUNET_DB_QueryStatus qs; 133 bool not_found; 134 bool refund_ok; 135 bool conflict; 136 bool gone; 137 138 /* Finally, store new refund data */ 139 qs = TEH_plugin->do_refund (TEH_plugin->cls, 140 refund, 141 &rctx->deposit_fee, 142 rctx->known_coin_id, 143 ¬_found, 144 &refund_ok, 145 &gone, 146 &conflict); 147 if (0 > qs) 148 { 149 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 150 *mhd_ret = TALER_MHD_reply_with_error (connection, 151 MHD_HTTP_INTERNAL_SERVER_ERROR, 152 TALER_EC_GENERIC_DB_FETCH_FAILED, 153 "do refund"); 154 return qs; 155 } 156 157 if (gone) 158 { 159 *mhd_ret = TALER_MHD_reply_with_error (connection, 160 MHD_HTTP_GONE, 161 TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, 162 NULL); 163 return GNUNET_DB_STATUS_HARD_ERROR; 164 } 165 if (conflict) 166 { 167 GNUNET_break_op (0); 168 *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( 169 connection, 170 TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, 171 &refund->coin.denom_pub_hash, 172 &refund->coin.coin_pub); 173 return GNUNET_DB_STATUS_HARD_ERROR; 174 } 175 if (not_found) 176 { 177 *mhd_ret = TALER_MHD_reply_with_error (connection, 178 MHD_HTTP_NOT_FOUND, 179 TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND, 180 NULL); 181 return GNUNET_DB_STATUS_HARD_ERROR; 182 } 183 if (! refund_ok) 184 { 185 *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( 186 connection, 187 TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, 188 &refund->coin.denom_pub_hash, 189 &refund->coin.coin_pub); 190 return GNUNET_DB_STATUS_HARD_ERROR; 191 } 192 return qs; 193 } 194 195 196 /** 197 * We have parsed the JSON information about the refund, do some basic 198 * sanity checks (especially that the signature on the coin is valid) 199 * and then execute the refund. Note that we need the DB to check 200 * the fee structure, so this is not done here. 201 * 202 * @param connection the MHD connection to handle 203 * @param[in,out] refund information about the refund 204 * @return MHD result code 205 */ 206 static MHD_RESULT 207 verify_and_execute_refund (struct MHD_Connection *connection, 208 struct TALER_EXCHANGEDB_Refund *refund) 209 { 210 struct RefundContext rctx = { 211 .refund = refund 212 }; 213 214 TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++; 215 if (GNUNET_OK != 216 TALER_merchant_refund_verify (&refund->coin.coin_pub, 217 &refund->details.h_contract_terms, 218 refund->details.rtransaction_id, 219 &refund->details.refund_amount, 220 &refund->details.merchant_pub, 221 &refund->details.merchant_sig)) 222 { 223 TALER_LOG_WARNING ("Invalid signature on refund request\n"); 224 return TALER_MHD_reply_with_error (connection, 225 MHD_HTTP_FORBIDDEN, 226 TALER_EC_EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID, 227 NULL); 228 } 229 230 /* Fetch the coin's denomination (hash) */ 231 for (unsigned int i = 0; i < MAX_RETRIES; i++) 232 { 233 enum GNUNET_DB_QueryStatus qs; 234 235 qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, 236 &refund->coin.coin_pub, 237 &rctx.known_coin_id, 238 &refund->coin.denom_pub_hash); 239 switch (qs) 240 { 241 case GNUNET_DB_STATUS_SOFT_ERROR: 242 if (i < MAX_RETRIES - 1) 243 continue; 244 /* otherwise: fall-through */ 245 case GNUNET_DB_STATUS_HARD_ERROR: 246 GNUNET_break (0); 247 return TALER_MHD_reply_with_error (connection, 248 MHD_HTTP_INTERNAL_SERVER_ERROR, 249 TALER_EC_GENERIC_DB_FETCH_FAILED, 250 "get_coin_denomination"); 251 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 252 { 253 MHD_RESULT res; 254 char *dhs; 255 256 GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); 257 dhs = GNUNET_STRINGS_data_to_string_alloc ( 258 &refund->coin.denom_pub_hash, 259 sizeof (refund->coin.denom_pub_hash)); 260 res = TALER_MHD_reply_with_error (connection, 261 MHD_HTTP_NOT_FOUND, 262 TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND, 263 dhs); 264 GNUNET_free (dhs); 265 return res; 266 } 267 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 268 break; 269 } 270 } 271 272 { 273 /* Obtain information about the coin's denomination! */ 274 struct TEH_DenominationKey *dk; 275 MHD_RESULT mret; 276 277 dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash, 278 connection, 279 &mret); 280 if (NULL == dk) 281 { 282 /* DKI not found, but we do have a coin with this DK in our database; 283 not good... */ 284 GNUNET_break (0); 285 return mret; 286 } 287 refund->details.refund_fee = dk->meta.fees.refund; 288 rctx.deposit_fee = dk->meta.fees.deposit; 289 } 290 291 /* Finally run the actual transaction logic */ 292 { 293 MHD_RESULT mhd_ret; 294 295 if (GNUNET_OK != 296 TEH_DB_run_transaction (connection, 297 "run refund", 298 TEH_MT_REQUEST_OTHER, 299 &mhd_ret, 300 &refund_transaction, 301 &rctx)) 302 { 303 return mhd_ret; 304 } 305 } 306 return reply_refund_success (connection, 307 &refund->coin.coin_pub, 308 &refund->details); 309 } 310 311 312 /** 313 * Handle a "/coins/$COIN_PUB/refund" request. Parses the JSON, and, if 314 * successful, passes the JSON data to #verify_and_execute_refund() to further 315 * check the details of the operation specified. If everything checks out, 316 * this will ultimately lead to the refund being executed, or rejected. 317 * 318 * @param connection the MHD connection to handle 319 * @param coin_pub public key of the coin 320 * @param root uploaded JSON data 321 * @return MHD result code 322 */ 323 MHD_RESULT 324 TEH_handler_refund (struct MHD_Connection *connection, 325 const struct TALER_CoinSpendPublicKeyP *coin_pub, 326 const json_t *root) 327 { 328 struct TALER_EXCHANGEDB_Refund refund = { 329 .details.refund_fee.currency = {0} /* set to invalid, just to be sure */ 330 }; 331 struct GNUNET_JSON_Specification spec[] = { 332 TALER_JSON_spec_amount ("refund_amount", 333 TEH_currency, 334 &refund.details.refund_amount), 335 GNUNET_JSON_spec_fixed_auto ("h_contract_terms", 336 &refund.details.h_contract_terms), 337 GNUNET_JSON_spec_fixed_auto ("merchant_pub", 338 &refund.details.merchant_pub), 339 GNUNET_JSON_spec_uint64 ("rtransaction_id", 340 &refund.details.rtransaction_id), 341 GNUNET_JSON_spec_fixed_auto ("merchant_sig", 342 &refund.details.merchant_sig), 343 GNUNET_JSON_spec_end () 344 }; 345 346 refund.coin.coin_pub = *coin_pub; 347 { 348 enum GNUNET_GenericReturnValue res; 349 350 res = TALER_MHD_parse_json_data (connection, 351 root, 352 spec); 353 if (GNUNET_SYSERR == res) 354 return MHD_NO; /* hard failure */ 355 if (GNUNET_NO == res) 356 return MHD_YES; /* failure */ 357 } 358 { 359 MHD_RESULT res; 360 361 res = verify_and_execute_refund (connection, 362 &refund); 363 GNUNET_JSON_parse_free (spec); 364 return res; 365 } 366 } 367 368 369 /* end of taler-exchange-httpd_refund.c */