Line data Source code
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 12 : 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 12 : if (TALER_EC_NONE !=
62 12 : (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 12 : refund->rtransaction_id,
68 : &refund->refund_amount,
69 : &pub,
70 : &sig)))
71 : {
72 0 : return TALER_MHD_reply_with_ec (connection,
73 : ec,
74 : NULL);
75 : }
76 12 : 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 16 : refund_transaction (void *cls,
127 : struct MHD_Connection *connection,
128 : MHD_RESULT *mhd_ret)
129 : {
130 16 : struct RefundContext *rctx = cls;
131 16 : 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 16 : qs = TEH_plugin->do_refund (TEH_plugin->cls,
140 : refund,
141 16 : &rctx->deposit_fee,
142 : rctx->known_coin_id,
143 : ¬_found,
144 : &refund_ok,
145 : &gone,
146 : &conflict);
147 16 : if (0 > qs)
148 : {
149 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
150 0 : *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 0 : return qs;
155 : }
156 :
157 16 : if (gone)
158 : {
159 2 : *mhd_ret = TALER_MHD_reply_with_error (connection,
160 : MHD_HTTP_GONE,
161 : TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID,
162 : NULL);
163 2 : return GNUNET_DB_STATUS_HARD_ERROR;
164 : }
165 14 : if (conflict)
166 : {
167 0 : GNUNET_break_op (0);
168 0 : *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 0 : return GNUNET_DB_STATUS_HARD_ERROR;
174 : }
175 14 : if (not_found)
176 : {
177 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
178 : MHD_HTTP_NOT_FOUND,
179 : TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND,
180 : NULL);
181 0 : return GNUNET_DB_STATUS_HARD_ERROR;
182 : }
183 14 : if (! refund_ok)
184 : {
185 2 : *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 2 : return GNUNET_DB_STATUS_HARD_ERROR;
191 : }
192 12 : 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 16 : verify_and_execute_refund (struct MHD_Connection *connection,
208 : struct TALER_EXCHANGEDB_Refund *refund)
209 : {
210 16 : struct RefundContext rctx = {
211 : .refund = refund
212 : };
213 :
214 16 : TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
215 16 : if (GNUNET_OK !=
216 16 : TALER_merchant_refund_verify (&refund->coin.coin_pub,
217 16 : &refund->details.h_contract_terms,
218 : refund->details.rtransaction_id,
219 16 : &refund->details.refund_amount,
220 16 : &refund->details.merchant_pub,
221 16 : &refund->details.merchant_sig))
222 : {
223 0 : TALER_LOG_WARNING ("Invalid signature on refund request\n");
224 0 : 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 64 : for (unsigned int i = 0; i < MAX_RETRIES; i++)
232 : {
233 : enum GNUNET_DB_QueryStatus qs;
234 :
235 48 : qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls,
236 48 : &refund->coin.coin_pub,
237 : &rctx.known_coin_id,
238 : &refund->coin.denom_pub_hash);
239 48 : switch (qs)
240 : {
241 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
242 0 : if (i < MAX_RETRIES - 1)
243 0 : continue;
244 : /* otherwise: fall-through */
245 : case GNUNET_DB_STATUS_HARD_ERROR:
246 0 : GNUNET_break (0);
247 0 : 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 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
252 : {
253 : MHD_RESULT res;
254 : char *dhs;
255 :
256 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
257 0 : dhs = GNUNET_STRINGS_data_to_string_alloc (
258 0 : &refund->coin.denom_pub_hash,
259 : sizeof (refund->coin.denom_pub_hash));
260 0 : res = TALER_MHD_reply_with_error (connection,
261 : MHD_HTTP_NOT_FOUND,
262 : TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND,
263 : dhs);
264 0 : GNUNET_free (dhs);
265 0 : return res;
266 : }
267 48 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
268 48 : break;
269 : }
270 : }
271 :
272 : {
273 : /* Obtain information about the coin's denomination! */
274 : struct TEH_DenominationKey *dk;
275 : MHD_RESULT mret;
276 :
277 16 : dk = TEH_keys_denomination_by_hash (&refund->coin.denom_pub_hash,
278 : connection,
279 : &mret);
280 16 : if (NULL == dk)
281 : {
282 : /* DKI not found, but we do have a coin with this DK in our database;
283 : not good... */
284 0 : GNUNET_break (0);
285 0 : return mret;
286 : }
287 16 : refund->details.refund_fee = dk->meta.fees.refund;
288 16 : rctx.deposit_fee = dk->meta.fees.deposit;
289 : }
290 :
291 : /* Finally run the actual transaction logic */
292 : {
293 : MHD_RESULT mhd_ret;
294 :
295 16 : if (GNUNET_OK !=
296 16 : TEH_DB_run_transaction (connection,
297 : "run refund",
298 : TEH_MT_REQUEST_OTHER,
299 : &mhd_ret,
300 : &refund_transaction,
301 : &rctx))
302 : {
303 4 : return mhd_ret;
304 : }
305 : }
306 12 : return reply_refund_success (connection,
307 12 : &refund->coin.coin_pub,
308 12 : &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 16 : TEH_handler_refund (struct MHD_Connection *connection,
325 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
326 : const json_t *root)
327 : {
328 16 : 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 16 : TALER_JSON_spec_amount ("refund_amount",
333 : TEH_currency,
334 : &refund.details.refund_amount),
335 16 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
336 : &refund.details.h_contract_terms),
337 16 : GNUNET_JSON_spec_fixed_auto ("merchant_pub",
338 : &refund.details.merchant_pub),
339 16 : GNUNET_JSON_spec_uint64 ("rtransaction_id",
340 : &refund.details.rtransaction_id),
341 16 : GNUNET_JSON_spec_fixed_auto ("merchant_sig",
342 : &refund.details.merchant_sig),
343 16 : GNUNET_JSON_spec_end ()
344 : };
345 :
346 16 : refund.coin.coin_pub = *coin_pub;
347 : {
348 : enum GNUNET_GenericReturnValue res;
349 :
350 16 : res = TALER_MHD_parse_json_data (connection,
351 : root,
352 : spec);
353 16 : if (GNUNET_SYSERR == res)
354 0 : return MHD_NO; /* hard failure */
355 16 : if (GNUNET_NO == res)
356 0 : return MHD_YES; /* failure */
357 : }
358 : {
359 : MHD_RESULT res;
360 :
361 16 : res = verify_and_execute_refund (connection,
362 : &refund);
363 16 : GNUNET_JSON_parse_free (spec);
364 16 : return res;
365 : }
366 : }
367 :
368 :
369 : /* end of taler-exchange-httpd_refund.c */
|