exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

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                               &not_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 */