Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2014-2021 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 : /**
20 : * @file src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c
21 : * @brief handling of POST /orders/$ID/abort requests
22 : * @author Marcello Stanisci
23 : * @author Christian Grothoff
24 : * @author Florian Dold
25 : */
26 : #include "platform.h"
27 : struct RefundDetails;
28 : #define TALER_EXCHANGE_POST_COINS_REFUND_RESULT_CLOSURE struct RefundDetails
29 : #include <taler/taler_json_lib.h>
30 : #include <taler/taler_exchange_service.h>
31 : #include "taler-merchant-httpd_exchanges.h"
32 : #include "taler-merchant-httpd_get-exchanges.h"
33 : #include "taler-merchant-httpd_helper.h"
34 : #include "taler-merchant-httpd_post-orders-ORDER_ID-abort.h"
35 : #include "merchant-database/iterate_deposits.h"
36 : #include "merchant-database/get_order_status.h"
37 : #include "merchant-database/insert_refund_by_coin.h"
38 : #include "merchant-database/preflight.h"
39 : #include "merchant-database/start.h"
40 : #include "merchant-database/set_instance.h"
41 :
42 :
43 : /**
44 : * How long to wait before giving up processing with the exchange?
45 : */
46 : #define ABORT_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \
47 : GNUNET_TIME_UNIT_SECONDS, \
48 : 30))
49 :
50 : /**
51 : * How often do we retry the (complex!) database transaction?
52 : */
53 : #define MAX_RETRIES 5
54 :
55 : /**
56 : * Information we keep for an individual call to the /abort handler.
57 : */
58 : struct AbortContext;
59 :
60 : /**
61 : * Information kept during a /abort request for each coin.
62 : */
63 : struct RefundDetails
64 : {
65 :
66 : /**
67 : * Public key of the coin.
68 : */
69 : struct TALER_CoinSpendPublicKeyP coin_pub;
70 :
71 : /**
72 : * Signature from the exchange confirming the refund.
73 : * Set if we were successful (status 200).
74 : */
75 : struct TALER_ExchangeSignatureP exchange_sig;
76 :
77 : /**
78 : * Public key used for @e exchange_sig.
79 : * Set if we were successful (status 200).
80 : */
81 : struct TALER_ExchangePublicKeyP exchange_pub;
82 :
83 : /**
84 : * Reference to the main AbortContext
85 : */
86 : struct AbortContext *ac;
87 :
88 : /**
89 : * Handle to the refund operation we are performing for
90 : * this coin, NULL after the operation is done.
91 : */
92 : struct TALER_EXCHANGE_PostCoinsRefundHandle *rh;
93 :
94 : /**
95 : * URL of the exchange that issued this coin.
96 : */
97 : char *exchange_url;
98 :
99 : /**
100 : * Body of the response from the exchange. Note that the body returned MUST
101 : * be freed (if non-NULL).
102 : */
103 : json_t *exchange_reply;
104 :
105 : /**
106 : * Amount this coin contributes to the total purchase price.
107 : * This amount includes the deposit fee.
108 : */
109 : struct TALER_Amount amount_with_fee;
110 :
111 : /**
112 : * Offset of this coin into the `rd` array of all coins in the
113 : * @e ac.
114 : */
115 : unsigned int index;
116 :
117 : /**
118 : * HTTP status returned by the exchange (if any).
119 : */
120 : unsigned int http_status;
121 :
122 : /**
123 : * Did we try to process this refund yet?
124 : */
125 : bool processed;
126 :
127 : /**
128 : * Did we find the deposit in our own database?
129 : */
130 : bool found_deposit;
131 : };
132 :
133 :
134 : /**
135 : * Information we keep for an individual call to the /abort handler.
136 : */
137 : struct AbortContext
138 : {
139 :
140 : /**
141 : * Hashed contract terms (according to client).
142 : */
143 : struct TALER_PrivateContractHashP h_contract_terms;
144 :
145 : /**
146 : * Context for our operation.
147 : */
148 : struct TMH_HandlerContext *hc;
149 :
150 : /**
151 : * Stored in a DLL.
152 : */
153 : struct AbortContext *next;
154 :
155 : /**
156 : * Stored in a DLL.
157 : */
158 : struct AbortContext *prev;
159 :
160 : /**
161 : * Array with @e coins_cnt coins we are despositing.
162 : */
163 : struct RefundDetails *rd;
164 :
165 : /**
166 : * MHD connection to return to
167 : */
168 : struct MHD_Connection *connection;
169 :
170 : /**
171 : * Task called when the (suspended) processing for
172 : * the /abort request times out.
173 : * Happens when we don't get a response from the exchange.
174 : */
175 : struct GNUNET_SCHEDULER_Task *timeout_task;
176 :
177 : /**
178 : * Response to return, NULL if we don't have one yet.
179 : */
180 : struct MHD_Response *response;
181 :
182 : /**
183 : * Handle to the exchange that we are doing the abortment with.
184 : * (initially NULL while @e fo is trying to find a exchange).
185 : */
186 : struct TALER_EXCHANGE_Handle *mh;
187 :
188 : /**
189 : * Handle for operation to lookup /keys (and auditors) from
190 : * the exchange used for this transaction; NULL if no operation is
191 : * pending.
192 : */
193 : struct TMH_EXCHANGES_KeysOperation *fo;
194 :
195 : /**
196 : * URL of the exchange used for the last @e fo.
197 : */
198 : const char *current_exchange;
199 :
200 : /**
201 : * Status of the database operations performed by the
202 : * #refund_coins() row callback. Set to
203 : * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT before starting the
204 : * iteration over the deposits, and to the failed query status if
205 : * one of the insert_refund_by_coin() operations failed. Inspected by
206 : * #begin_transaction() once the iteration has finished.
207 : */
208 : enum GNUNET_DB_QueryStatus row_qs;
209 :
210 : /**
211 : * Number of coins this abort is for. Length of the @e rd array.
212 : */
213 : size_t coins_cnt;
214 :
215 : /**
216 : * How often have we retried the 'main' transaction?
217 : */
218 : unsigned int retry_counter;
219 :
220 : /**
221 : * Number of transactions still pending. Initially set to
222 : * @e coins_cnt, decremented on each transaction that
223 : * successfully finished.
224 : */
225 : size_t pending;
226 :
227 : /**
228 : * Number of transactions still pending for the currently selected
229 : * exchange. Initially set to the number of coins started at the
230 : * exchange, decremented on each transaction that successfully
231 : * finished. Once it hits zero, we pick the next exchange.
232 : */
233 : size_t pending_at_ce;
234 :
235 : /**
236 : * HTTP status code to use for the reply, i.e 200 for "OK".
237 : * Special value UINT_MAX is used to indicate hard errors
238 : * (no reply, return #MHD_NO).
239 : */
240 : unsigned int response_code;
241 :
242 : /**
243 : * #GNUNET_NO if the @e connection was not suspended,
244 : * #GNUNET_YES if the @e connection was suspended,
245 : * #GNUNET_SYSERR if @e connection was resumed to as
246 : * part of #MH_force_ac_resume during shutdown.
247 : */
248 : int suspended;
249 :
250 : };
251 :
252 :
253 : /**
254 : * Head of active abort context DLL.
255 : */
256 : static struct AbortContext *ac_head;
257 :
258 : /**
259 : * Tail of active abort context DLL.
260 : */
261 : static struct AbortContext *ac_tail;
262 :
263 :
264 : /**
265 : * Abort all pending /deposit operations.
266 : *
267 : * @param ac abort context to abort
268 : */
269 : static void
270 4 : abort_refunds (struct AbortContext *ac)
271 : {
272 4 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
273 : "Aborting pending /deposit operations\n");
274 8 : for (size_t i = 0; i<ac->coins_cnt; i++)
275 : {
276 4 : struct RefundDetails *rdi = &ac->rd[i];
277 :
278 4 : if (NULL != rdi->rh)
279 : {
280 0 : TALER_EXCHANGE_post_coins_refund_cancel (rdi->rh);
281 0 : rdi->rh = NULL;
282 : }
283 : }
284 4 : }
285 :
286 :
287 : void
288 20 : TMH_force_ac_resume ()
289 : {
290 20 : for (struct AbortContext *ac = ac_head;
291 20 : NULL != ac;
292 0 : ac = ac->next)
293 : {
294 0 : abort_refunds (ac);
295 0 : if (NULL != ac->timeout_task)
296 : {
297 0 : GNUNET_SCHEDULER_cancel (ac->timeout_task);
298 0 : ac->timeout_task = NULL;
299 : }
300 0 : if (GNUNET_YES == ac->suspended)
301 : {
302 0 : ac->suspended = GNUNET_SYSERR;
303 0 : MHD_resume_connection (ac->connection);
304 : }
305 : }
306 20 : }
307 :
308 :
309 : /**
310 : * Resume the given abort context and send the given response.
311 : * Stores the response in the @a ac and signals MHD to resume
312 : * the connection. Also ensures MHD runs immediately.
313 : *
314 : * @param ac abortment context
315 : * @param response_code response code to use
316 : * @param response response data to send back
317 : */
318 : static void
319 2 : resume_abort_with_response (struct AbortContext *ac,
320 : unsigned int response_code,
321 : struct MHD_Response *response)
322 : {
323 2 : abort_refunds (ac);
324 2 : ac->response_code = response_code;
325 2 : ac->response = response;
326 2 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
327 : "Resuming /abort handling as exchange interaction is done (%u)\n",
328 : response_code);
329 2 : if (NULL != ac->timeout_task)
330 : {
331 2 : GNUNET_SCHEDULER_cancel (ac->timeout_task);
332 2 : ac->timeout_task = NULL;
333 : }
334 2 : GNUNET_assert (GNUNET_YES == ac->suspended);
335 2 : ac->suspended = GNUNET_NO;
336 2 : MHD_resume_connection (ac->connection);
337 2 : TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
338 2 : }
339 :
340 :
341 : /**
342 : * Resume abortment processing with an error.
343 : *
344 : * @param ac operation to resume
345 : * @param http_status http status code to return
346 : * @param ec taler error code to return
347 : * @param msg human readable error message
348 : */
349 : static void
350 0 : resume_abort_with_error (struct AbortContext *ac,
351 : unsigned int http_status,
352 : enum TALER_ErrorCode ec,
353 : const char *msg)
354 : {
355 0 : resume_abort_with_response (ac,
356 : http_status,
357 : TALER_MHD_make_error (ec,
358 : msg));
359 0 : }
360 :
361 :
362 : /**
363 : * Generate a response that indicates abortment success.
364 : *
365 : * @param ac abortment context
366 : */
367 : static void
368 2 : generate_success_response (struct AbortContext *ac)
369 : {
370 : json_t *refunds;
371 2 : unsigned int hc = MHD_HTTP_OK;
372 :
373 2 : refunds = json_array ();
374 2 : if (NULL == refunds)
375 : {
376 0 : GNUNET_break (0);
377 0 : resume_abort_with_error (ac,
378 : MHD_HTTP_INTERNAL_SERVER_ERROR,
379 : TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE,
380 : "could not create JSON array");
381 0 : return;
382 : }
383 4 : for (size_t i = 0; i<ac->coins_cnt; i++)
384 : {
385 2 : struct RefundDetails *rdi = &ac->rd[i];
386 : json_t *detail;
387 :
388 2 : if (rdi->found_deposit)
389 : {
390 2 : if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) &&
391 0 : (MHD_HTTP_NOT_FOUND != rdi->http_status) &&
392 0 : (MHD_HTTP_GONE != rdi->http_status) ) ||
393 2 : (0 == rdi->http_status) ||
394 2 : (NULL == rdi->exchange_reply) )
395 : {
396 0 : hc = MHD_HTTP_BAD_GATEWAY;
397 : }
398 : }
399 2 : if (! rdi->found_deposit)
400 : {
401 0 : detail = GNUNET_JSON_PACK (
402 : GNUNET_JSON_pack_string ("type",
403 : "undeposited"));
404 : }
405 : else
406 : {
407 2 : if (MHD_HTTP_OK != rdi->http_status)
408 : {
409 0 : detail = GNUNET_JSON_PACK (
410 : GNUNET_JSON_pack_string ("type",
411 : "failure"),
412 : GNUNET_JSON_pack_uint64 ("exchange_status",
413 : rdi->http_status),
414 : GNUNET_JSON_pack_uint64 ("exchange_code",
415 : (NULL != rdi->exchange_reply)
416 : ? TALER_JSON_get_error_code (
417 : rdi->exchange_reply)
418 : : TALER_EC_GENERIC_INVALID_RESPONSE),
419 : GNUNET_JSON_pack_allow_null (
420 : GNUNET_JSON_pack_object_incref ("exchange_reply",
421 : rdi->exchange_reply)));
422 : }
423 : else
424 : {
425 2 : detail = GNUNET_JSON_PACK (
426 : GNUNET_JSON_pack_string ("type",
427 : "success"),
428 : GNUNET_JSON_pack_uint64 ("exchange_status",
429 : rdi->http_status),
430 : GNUNET_JSON_pack_data_auto ("exchange_sig",
431 : &rdi->exchange_sig),
432 : GNUNET_JSON_pack_data_auto ("exchange_pub",
433 : &rdi->exchange_pub));
434 : }
435 : }
436 2 : GNUNET_assert (0 ==
437 : json_array_append_new (refunds,
438 : detail));
439 : }
440 :
441 : /* Resume and send back the response. */
442 2 : resume_abort_with_response (
443 : ac,
444 : hc,
445 2 : TALER_MHD_MAKE_JSON_PACK (
446 : GNUNET_JSON_pack_array_steal ("refunds",
447 : refunds)));
448 : }
449 :
450 :
451 : /**
452 : * Custom cleanup routine for a `struct AbortContext`.
453 : *
454 : * @param cls the `struct AbortContext` to clean up.
455 : */
456 : static void
457 2 : abort_context_cleanup (void *cls)
458 : {
459 2 : struct AbortContext *ac = cls;
460 :
461 2 : if (NULL != ac->timeout_task)
462 : {
463 0 : GNUNET_SCHEDULER_cancel (ac->timeout_task);
464 0 : ac->timeout_task = NULL;
465 : }
466 2 : abort_refunds (ac);
467 4 : for (size_t i = 0; i<ac->coins_cnt; i++)
468 : {
469 2 : struct RefundDetails *rdi = &ac->rd[i];
470 :
471 2 : if (NULL != rdi->exchange_reply)
472 : {
473 2 : json_decref (rdi->exchange_reply);
474 2 : rdi->exchange_reply = NULL;
475 : }
476 2 : GNUNET_free (rdi->exchange_url);
477 : }
478 2 : GNUNET_free (ac->rd);
479 2 : if (NULL != ac->fo)
480 : {
481 0 : TMH_EXCHANGES_keys4exchange_cancel (ac->fo);
482 0 : ac->fo = NULL;
483 : }
484 2 : if (NULL != ac->response)
485 : {
486 0 : MHD_destroy_response (ac->response);
487 0 : ac->response = NULL;
488 : }
489 2 : GNUNET_CONTAINER_DLL_remove (ac_head,
490 : ac_tail,
491 : ac);
492 2 : GNUNET_free (ac);
493 2 : }
494 :
495 :
496 : /**
497 : * Find the exchange we need to talk to for the next
498 : * pending deposit permission.
499 : *
500 : * @param ac abortment context we are processing
501 : */
502 : static void
503 : find_next_exchange (struct AbortContext *ac);
504 :
505 :
506 : /**
507 : * Function called with the result from the exchange (to be
508 : * passed back to the wallet).
509 : *
510 : * @param cls closure
511 : * @param rr response data
512 : */
513 : static void
514 2 : refund_cb (struct RefundDetails *rd,
515 : const struct TALER_EXCHANGE_PostCoinsRefundResponse *rr)
516 : {
517 2 : const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;
518 2 : struct AbortContext *ac = rd->ac;
519 :
520 2 : rd->rh = NULL;
521 2 : rd->http_status = hr->http_status;
522 2 : rd->exchange_reply = json_incref ((json_t*) hr->reply);
523 2 : if (MHD_HTTP_OK == hr->http_status)
524 : {
525 2 : rd->exchange_pub = rr->details.ok.exchange_pub;
526 2 : rd->exchange_sig = rr->details.ok.exchange_sig;
527 : }
528 2 : ac->pending_at_ce--;
529 2 : if (0 == ac->pending_at_ce)
530 2 : find_next_exchange (ac);
531 2 : }
532 :
533 :
534 : /**
535 : * Function called with the result of our exchange lookup.
536 : *
537 : * @param cls the `struct AbortContext`
538 : * @param keys keys of the exchange
539 : * @param exchange representation of the exchange
540 : */
541 : static void
542 2 : process_abort_with_exchange (void *cls,
543 : struct TALER_EXCHANGE_Keys *keys,
544 : struct TMH_Exchange *exchange)
545 : {
546 2 : struct AbortContext *ac = cls;
547 :
548 : (void) exchange;
549 2 : ac->fo = NULL;
550 2 : GNUNET_assert (GNUNET_YES == ac->suspended);
551 2 : if (NULL == keys)
552 : {
553 0 : resume_abort_with_response (
554 : ac,
555 : MHD_HTTP_GATEWAY_TIMEOUT,
556 : TALER_MHD_make_error (
557 : TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
558 : NULL));
559 0 : return;
560 : }
561 : /* Initiate refund operation for all coins of
562 : the current exchange (!) */
563 2 : GNUNET_assert (0 == ac->pending_at_ce);
564 4 : for (size_t i = 0; i<ac->coins_cnt; i++)
565 : {
566 2 : struct RefundDetails *rdi = &ac->rd[i];
567 :
568 2 : if (rdi->processed)
569 0 : continue;
570 2 : GNUNET_assert (NULL == rdi->rh);
571 2 : if (0 != strcmp (rdi->exchange_url,
572 : ac->current_exchange))
573 0 : continue;
574 2 : rdi->processed = true;
575 2 : ac->pending--;
576 2 : if (! rdi->found_deposit)
577 : {
578 : /* Coin wasn't even deposited yet, we do not need to refund it. */
579 0 : continue;
580 : }
581 4 : rdi->rh = TALER_EXCHANGE_post_coins_refund_create (
582 : TMH_curl_ctx,
583 : ac->current_exchange,
584 : keys,
585 2 : &rdi->amount_with_fee,
586 2 : &ac->h_contract_terms,
587 2 : &rdi->coin_pub,
588 : 0, /* rtransaction_id */
589 2 : &ac->hc->instance->merchant_priv);
590 2 : if (NULL == rdi->rh)
591 : {
592 0 : GNUNET_break_op (0);
593 0 : resume_abort_with_error (ac,
594 : MHD_HTTP_INTERNAL_SERVER_ERROR,
595 : TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED,
596 : "Failed to start refund with exchange");
597 0 : return;
598 : }
599 2 : GNUNET_assert (TALER_EC_NONE ==
600 : TALER_EXCHANGE_post_coins_refund_start (rdi->rh,
601 : &refund_cb,
602 : rdi));
603 2 : ac->pending_at_ce++;
604 : }
605 : /* Still continue if no coins for this exchange were deposited. */
606 2 : if (0 == ac->pending_at_ce)
607 0 : find_next_exchange (ac);
608 : }
609 :
610 :
611 : /**
612 : * Begin of the DB transaction. If required (from
613 : * soft/serialization errors), the transaction can be
614 : * restarted here.
615 : *
616 : * @param ac abortment context to transact
617 : */
618 : static void
619 : begin_transaction (struct AbortContext *ac);
620 :
621 :
622 : /**
623 : * Find the exchange we need to talk to for the next
624 : * pending deposit permission.
625 : *
626 : * @param ac abortment context we are processing
627 : */
628 : static void
629 4 : find_next_exchange (struct AbortContext *ac)
630 : {
631 6 : for (size_t i = 0; i<ac->coins_cnt; i++)
632 : {
633 4 : struct RefundDetails *rdi = &ac->rd[i];
634 :
635 4 : if (! rdi->processed)
636 : {
637 2 : ac->current_exchange = rdi->exchange_url;
638 2 : ac->fo = TMH_EXCHANGES_keys4exchange (ac->current_exchange,
639 : false,
640 : &process_abort_with_exchange,
641 : ac);
642 2 : if (NULL == ac->fo)
643 : {
644 : /* strange, should have happened on pay! */
645 0 : GNUNET_break (0);
646 0 : resume_abort_with_error (ac,
647 : MHD_HTTP_INTERNAL_SERVER_ERROR,
648 : TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED,
649 : ac->current_exchange);
650 0 : return;
651 : }
652 2 : return;
653 : }
654 : }
655 2 : ac->current_exchange = NULL;
656 2 : GNUNET_assert (0 == ac->pending);
657 : /* We are done with all the HTTP requests, go back and try
658 : the 'big' database transaction! (It should work now!) */
659 2 : begin_transaction (ac);
660 : }
661 :
662 :
663 : /**
664 : * Function called with information about a coin that was deposited.
665 : * Note that we must not roll back or restart the transaction here, as
666 : * we are called from within the iteration over the deposits. Failures
667 : * are merely recorded in @a ac->row_qs and then handled by
668 : * #begin_transaction() once the iteration has completed.
669 : *
670 : * @param cls closure
671 : * @param exchange_url exchange where @a coin_pub was deposited
672 : * @param coin_pub public key of the coin
673 : * @param amount_with_fee amount the exchange will deposit for this coin
674 : * @param deposit_fee fee the exchange will charge for this coin
675 : * @param refund_fee fee the exchange will charge for refunding this coin
676 : * @param wire_fee fee to be paid for the wire transfer
677 : */
678 : static void
679 4 : refund_coins (void *cls,
680 : const char *exchange_url,
681 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
682 : const struct TALER_Amount *amount_with_fee,
683 : const struct TALER_Amount *deposit_fee,
684 : const struct TALER_Amount *refund_fee,
685 : const struct TALER_Amount *wire_fee)
686 : {
687 4 : struct AbortContext *ac = cls;
688 : struct GNUNET_TIME_Timestamp now;
689 :
690 : (void) deposit_fee;
691 : (void) refund_fee;
692 : (void) wire_fee;
693 4 : if (0 > ac->row_qs)
694 0 : return; /* already failed, do not touch the DB any further */
695 4 : now = GNUNET_TIME_timestamp_get ();
696 8 : for (size_t i = 0; i<ac->coins_cnt; i++)
697 : {
698 4 : struct RefundDetails *rdi = &ac->rd[i];
699 : enum GNUNET_DB_QueryStatus qs;
700 :
701 4 : if ( (0 !=
702 4 : GNUNET_memcmp (coin_pub,
703 4 : &rdi->coin_pub)) ||
704 : (0 !=
705 4 : strcmp (exchange_url,
706 4 : rdi->exchange_url)) )
707 0 : continue; /* not in request */
708 4 : rdi->found_deposit = true;
709 4 : rdi->amount_with_fee = *amount_with_fee;
710 : /* Store refund in DB */
711 4 : qs = TALER_MERCHANTDB_insert_refund_by_coin (TMH_db,
712 4 : ac->hc->instance->settings.id,
713 4 : &ac->h_contract_terms,
714 : now,
715 : coin_pub,
716 : /* justification */
717 : "incomplete abortment aborted");
718 4 : if (0 > qs)
719 : {
720 : /* Always report on hard error to enable diagnostics */
721 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
722 0 : ac->row_qs = qs;
723 0 : return;
724 : }
725 : } /* for all coins */
726 : }
727 :
728 :
729 : /**
730 : * Begin of the DB transaction. If required (from soft/serialization errors),
731 : * the transaction can be restarted here.
732 : *
733 : * @param ac abortment context to transact
734 : */
735 : static void
736 4 : begin_transaction (struct AbortContext *ac)
737 : {
738 : enum GNUNET_DB_QueryStatus qs;
739 :
740 : /* Avoid re-trying transactions on soft errors forever! */
741 4 : if (ac->retry_counter++ > MAX_RETRIES)
742 : {
743 0 : GNUNET_break (0);
744 0 : resume_abort_with_error (ac,
745 : MHD_HTTP_INTERNAL_SERVER_ERROR,
746 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
747 : NULL);
748 0 : return;
749 : }
750 4 : GNUNET_assert (GNUNET_YES == ac->suspended);
751 :
752 : /* First, try to see if we have all we need already done */
753 4 : TALER_MERCHANTDB_preflight (TMH_db);
754 4 : qs = TALER_MERCHANTDB_set_instance (
755 : TMH_db,
756 4 : ac->hc->instance->settings.id);
757 4 : if (0 >= qs)
758 : {
759 0 : GNUNET_break (0);
760 0 : resume_abort_with_error (ac,
761 : MHD_HTTP_INTERNAL_SERVER_ERROR,
762 : TALER_EC_GENERIC_DB_START_FAILED,
763 : "set_instance");
764 0 : goto cleanup;
765 : }
766 4 : if (GNUNET_OK !=
767 4 : TALER_MERCHANTDB_start (TMH_db,
768 : "run abort"))
769 : {
770 0 : GNUNET_break (0);
771 0 : resume_abort_with_error (ac,
772 : MHD_HTTP_INTERNAL_SERVER_ERROR,
773 : TALER_EC_GENERIC_DB_START_FAILED,
774 : NULL);
775 0 : goto cleanup;
776 : }
777 :
778 : /* check payment was indeed incomplete
779 : (now that we are in the transaction scope!) */
780 : {
781 : struct TALER_PrivateContractHashP h_contract_terms;
782 : bool paid;
783 :
784 4 : qs = TALER_MERCHANTDB_get_order_status (TMH_db,
785 4 : ac->hc->instance->settings.id,
786 4 : ac->hc->infix,
787 : &h_contract_terms,
788 : &paid);
789 4 : switch (qs)
790 : {
791 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
792 : case GNUNET_DB_STATUS_HARD_ERROR:
793 : /* Always report on hard error to enable diagnostics */
794 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
795 0 : TALER_MERCHANTDB_rollback (TMH_db);
796 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
797 : {
798 0 : begin_transaction (ac);
799 0 : goto cleanup;
800 : }
801 : /* Always report on hard error as well to enable diagnostics */
802 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
803 0 : resume_abort_with_error (ac,
804 : MHD_HTTP_INTERNAL_SERVER_ERROR,
805 : TALER_EC_GENERIC_DB_FETCH_FAILED,
806 : "order status");
807 0 : goto cleanup;
808 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
809 0 : TALER_MERCHANTDB_rollback (TMH_db);
810 0 : resume_abort_with_error (ac,
811 : MHD_HTTP_NOT_FOUND,
812 : TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND,
813 : "Could not find contract");
814 0 : goto cleanup;
815 4 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
816 4 : if (paid)
817 : {
818 : /* Payment is complete, refuse to abort. */
819 0 : TALER_MERCHANTDB_rollback (TMH_db);
820 0 : resume_abort_with_error (ac,
821 : MHD_HTTP_PRECONDITION_FAILED,
822 : TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE,
823 : "Payment was complete, refusing to abort");
824 0 : goto cleanup;
825 : }
826 : }
827 4 : if (0 !=
828 4 : GNUNET_memcmp (&ac->h_contract_terms,
829 : &h_contract_terms))
830 : {
831 0 : GNUNET_break_op (0);
832 0 : TALER_MERCHANTDB_rollback (TMH_db);
833 0 : resume_abort_with_error (ac,
834 : MHD_HTTP_FORBIDDEN,
835 : TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH,
836 : "Provided hash does not match order on file");
837 0 : goto cleanup;
838 : }
839 : }
840 :
841 : /* Mark all deposits we have in our database for the order as refunded. */
842 4 : ac->row_qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
843 4 : qs = TALER_MERCHANTDB_iterate_deposits (TMH_db,
844 4 : ac->hc->instance->settings.id,
845 4 : &ac->h_contract_terms,
846 : &refund_coins,
847 : ac);
848 4 : if (0 > qs)
849 : {
850 0 : TALER_MERCHANTDB_rollback (TMH_db);
851 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
852 : {
853 0 : begin_transaction (ac);
854 0 : goto cleanup;
855 : }
856 : /* Always report on hard error as well to enable diagnostics */
857 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
858 0 : resume_abort_with_error (ac,
859 : MHD_HTTP_INTERNAL_SERVER_ERROR,
860 : TALER_EC_GENERIC_DB_FETCH_FAILED,
861 : "deposits");
862 0 : goto cleanup;
863 : }
864 : /* Check if one of the insert_refund_by_coin() operations done by the
865 : #refund_coins() row callback failed; we must handle this here (and
866 : not in the callback) as we may only roll back and restart the
867 : transaction from outside of the row iteration. */
868 4 : if (0 > ac->row_qs)
869 : {
870 0 : TALER_MERCHANTDB_rollback (TMH_db);
871 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == ac->row_qs)
872 : {
873 0 : begin_transaction (ac);
874 0 : goto cleanup;
875 : }
876 : /* Always report on hard error as well to enable diagnostics */
877 0 : GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == ac->row_qs);
878 0 : resume_abort_with_error (ac,
879 : MHD_HTTP_INTERNAL_SERVER_ERROR,
880 : TALER_EC_GENERIC_DB_STORE_FAILED,
881 : "insert_refund_by_coin");
882 0 : goto cleanup;
883 : }
884 :
885 4 : qs = TALER_MERCHANTDB_commit (TMH_db);
886 4 : if (0 > qs)
887 : {
888 0 : TALER_MERCHANTDB_rollback (TMH_db);
889 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
890 : {
891 0 : begin_transaction (ac);
892 0 : goto cleanup;
893 : }
894 0 : resume_abort_with_error (ac,
895 : MHD_HTTP_INTERNAL_SERVER_ERROR,
896 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
897 : NULL);
898 0 : goto cleanup;
899 : }
900 :
901 : /* At this point, the refund got correctly committed
902 : into the database. Tell exchange about abort/refund. */
903 4 : if (ac->pending > 0)
904 : {
905 2 : find_next_exchange (ac);
906 2 : goto cleanup;
907 : }
908 2 : generate_success_response (ac);
909 4 : cleanup:
910 4 : GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
911 : TALER_MERCHANTDB_set_instance (
912 : TMH_db,
913 : NULL));
914 : }
915 :
916 :
917 : /**
918 : * Try to parse the abort request into the given abort context.
919 : * Schedules an error response in the connection on failure.
920 : *
921 : * @param connection HTTP connection we are receiving abortment on
922 : * @param hc context we use to handle the abortment
923 : * @param ac state of the /abort call
924 : * @return #GNUNET_OK on success,
925 : * #GNUNET_NO on failure (response was queued with MHD)
926 : * #GNUNET_SYSERR on hard error (MHD connection must be dropped)
927 : */
928 : static enum GNUNET_GenericReturnValue
929 2 : parse_abort (struct MHD_Connection *connection,
930 : struct TMH_HandlerContext *hc,
931 : struct AbortContext *ac)
932 : {
933 : const json_t *coins;
934 : struct GNUNET_JSON_Specification spec[] = {
935 2 : GNUNET_JSON_spec_array_const ("coins",
936 : &coins),
937 2 : GNUNET_JSON_spec_fixed_auto ("h_contract",
938 : &ac->h_contract_terms),
939 :
940 2 : GNUNET_JSON_spec_end ()
941 : };
942 : enum GNUNET_GenericReturnValue res;
943 :
944 2 : res = TALER_MHD_parse_json_data (connection,
945 2 : hc->request_body,
946 : spec);
947 2 : if (GNUNET_YES != res)
948 : {
949 0 : GNUNET_break_op (0);
950 0 : return res;
951 : }
952 2 : ac->coins_cnt = json_array_size (coins);
953 2 : if (0 == ac->coins_cnt)
954 : {
955 0 : GNUNET_break_op (0);
956 : return (MHD_YES ==
957 0 : TALER_MHD_reply_with_error (connection,
958 : MHD_HTTP_BAD_REQUEST,
959 : TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY,
960 : "coins"))
961 : ? GNUNET_NO
962 0 : : GNUNET_SYSERR;
963 : }
964 : /* note: 1 coin = 1 deposit confirmation expected */
965 2 : ac->pending = ac->coins_cnt;
966 2 : ac->rd = GNUNET_new_array (ac->coins_cnt,
967 : struct RefundDetails);
968 : /* This loop populates the array 'rd' in 'ac' */
969 : {
970 : unsigned int coins_index;
971 : json_t *coin;
972 4 : json_array_foreach (coins, coins_index, coin)
973 : {
974 2 : struct RefundDetails *rd = &ac->rd[coins_index];
975 : const char *exchange_url;
976 : struct GNUNET_JSON_Specification ispec[] = {
977 2 : TALER_JSON_spec_web_url ("exchange_url",
978 : &exchange_url),
979 2 : GNUNET_JSON_spec_fixed_auto ("coin_pub",
980 : &rd->coin_pub),
981 2 : GNUNET_JSON_spec_end ()
982 : };
983 :
984 2 : res = TALER_MHD_parse_json_data (connection,
985 : coin,
986 : ispec);
987 2 : if (GNUNET_YES != res)
988 : {
989 0 : GNUNET_break_op (0);
990 0 : return res;
991 : }
992 2 : rd->exchange_url = GNUNET_strdup (exchange_url);
993 2 : rd->index = coins_index;
994 2 : rd->ac = ac;
995 : }
996 : }
997 2 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
998 : "Handling /abort for order `%s' with contract hash `%s'\n",
999 : ac->hc->infix,
1000 : GNUNET_h2s (&ac->h_contract_terms.hash));
1001 2 : return GNUNET_OK;
1002 : }
1003 :
1004 :
1005 : /**
1006 : * Handle a timeout for the processing of the abort request.
1007 : *
1008 : * @param cls our `struct AbortContext`
1009 : */
1010 : static void
1011 0 : handle_abort_timeout (void *cls)
1012 : {
1013 0 : struct AbortContext *ac = cls;
1014 :
1015 0 : ac->timeout_task = NULL;
1016 0 : GNUNET_assert (GNUNET_YES == ac->suspended);
1017 0 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
1018 : "Resuming abort with error after timeout\n");
1019 0 : if (NULL != ac->fo)
1020 : {
1021 0 : TMH_EXCHANGES_keys4exchange_cancel (ac->fo);
1022 0 : ac->fo = NULL;
1023 : }
1024 0 : resume_abort_with_error (ac,
1025 : MHD_HTTP_GATEWAY_TIMEOUT,
1026 : TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
1027 : NULL);
1028 0 : }
1029 :
1030 :
1031 : enum MHD_Result
1032 4 : TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh,
1033 : struct MHD_Connection *connection,
1034 : struct TMH_HandlerContext *hc)
1035 : {
1036 4 : struct AbortContext *ac = hc->ctx;
1037 :
1038 4 : if (NULL == ac)
1039 : {
1040 2 : ac = GNUNET_new (struct AbortContext);
1041 2 : GNUNET_CONTAINER_DLL_insert (ac_head,
1042 : ac_tail,
1043 : ac);
1044 2 : ac->connection = connection;
1045 2 : ac->hc = hc;
1046 2 : hc->ctx = ac;
1047 2 : hc->cc = &abort_context_cleanup;
1048 : }
1049 4 : if (GNUNET_SYSERR == ac->suspended)
1050 0 : return MHD_NO; /* during shutdown, we don't generate any more replies */
1051 4 : if (0 != ac->response_code)
1052 : {
1053 : enum MHD_Result res;
1054 :
1055 : /* We are *done* processing the request,
1056 : just queue the response (!) */
1057 2 : if (UINT_MAX == ac->response_code)
1058 : {
1059 0 : GNUNET_break (0);
1060 0 : return MHD_NO; /* hard error */
1061 : }
1062 2 : res = MHD_queue_response (connection,
1063 : ac->response_code,
1064 : ac->response);
1065 2 : MHD_destroy_response (ac->response);
1066 2 : ac->response = NULL;
1067 2 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
1068 : "Queueing response (%u) for /abort (%s).\n",
1069 : (unsigned int) ac->response_code,
1070 : res ? "OK" : "FAILED");
1071 2 : return res;
1072 : }
1073 : {
1074 : enum GNUNET_GenericReturnValue ret;
1075 :
1076 2 : ret = parse_abort (connection,
1077 : hc,
1078 : ac);
1079 2 : if (GNUNET_OK != ret)
1080 : return (GNUNET_NO == ret)
1081 : ? MHD_YES
1082 0 : : MHD_NO;
1083 : }
1084 :
1085 : /* Abort not finished, suspend while we interact with the exchange */
1086 2 : GNUNET_assert (GNUNET_NO == ac->suspended);
1087 2 : MHD_suspend_connection (connection);
1088 2 : ac->suspended = GNUNET_YES;
1089 2 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
1090 : "Suspending abort handling while working with the exchange\n");
1091 2 : ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_GENERIC_TIMEOUT,
1092 : &handle_abort_timeout,
1093 : ac);
1094 2 : begin_transaction (ac);
1095 2 : return MHD_YES;
1096 : }
1097 :
1098 :
1099 : /* end of taler-merchant-httpd_post-orders-ORDER_ID-abort.c */
|