Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2014-2026 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Lesser General Public License as
7 : published by the Free Software Foundation; either version 2.1,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful,
11 : but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU Lesser General Public License for more details.
14 :
15 : You should have received a copy of the GNU Lesser General
16 : Public License along with TALER; see the file COPYING.LGPL.
17 : If not, see <http://www.gnu.org/licenses/>
18 : */
19 : /**
20 : * @file src/lib/merchant_api_post-orders-ORDER_ID-abort.c
21 : * @brief Implementation of the POST /orders/$ID/abort request
22 : * @author Christian Grothoff
23 : */
24 : #include "platform.h"
25 : #include <curl/curl.h>
26 : #include <jansson.h>
27 : #include <microhttpd.h> /* just for HTTP status codes */
28 : #include <gnunet/gnunet_util_lib.h>
29 : #include <gnunet/gnunet_curl_lib.h>
30 : #include <taler/merchant/post-orders-ORDER_ID-abort.h>
31 : #include "merchant_api_curl_defaults.h"
32 : #include "merchant_api_common.h"
33 : #include <taler/taler_json_lib.h>
34 : #include <taler/taler_curl_lib.h>
35 : #include <taler/taler_signatures.h>
36 :
37 :
38 : /**
39 : * Maximum number of refunds we return.
40 : */
41 : #define MAX_REFUNDS 1024
42 :
43 :
44 : /**
45 : * Handle for a POST /orders/$ORDER_ID/abort operation.
46 : */
47 : struct TALER_MERCHANT_PostOrdersAbortHandle
48 : {
49 : /**
50 : * Base URL of the merchant backend.
51 : */
52 : char *base_url;
53 :
54 : /**
55 : * The full URL for this request.
56 : */
57 : char *url;
58 :
59 : /**
60 : * Handle for the request.
61 : */
62 : struct GNUNET_CURL_Job *job;
63 :
64 : /**
65 : * Function to call with the result.
66 : */
67 : TALER_MERCHANT_PostOrdersAbortCallback cb;
68 :
69 : /**
70 : * Closure for @a cb.
71 : */
72 : TALER_MERCHANT_POST_ORDERS_ABORT_RESULT_CLOSURE *cb_cls;
73 :
74 : /**
75 : * Reference to the execution context.
76 : */
77 : struct GNUNET_CURL_Context *ctx;
78 :
79 : /**
80 : * Minor context that holds body and headers.
81 : */
82 : struct TALER_CURL_PostContext post_ctx;
83 :
84 : /**
85 : * Order identifier.
86 : */
87 : char *order_id;
88 :
89 : /**
90 : * Public key of the merchant.
91 : */
92 : struct TALER_MerchantPublicKeyP merchant_pub;
93 :
94 : /**
95 : * Hash of the contract terms.
96 : */
97 : struct TALER_PrivateContractHashP h_contract;
98 :
99 : /**
100 : * The coins we are aborting on.
101 : */
102 : struct TALER_MERCHANT_PostOrdersAbortCoin *coins;
103 :
104 : /**
105 : * Number of @e coins.
106 : */
107 : unsigned int num_coins;
108 : };
109 :
110 :
111 : /**
112 : * Check that the response for an abort is well-formed,
113 : * and call the application callback with the result if it is
114 : * OK. Otherwise returns #GNUNET_SYSERR.
115 : *
116 : * @param poah handle to operation that created the reply
117 : * @param[in] ar abort response, partially initialized
118 : * @param json the reply to parse
119 : * @return #GNUNET_OK on success
120 : */
121 : static enum GNUNET_GenericReturnValue
122 2 : check_abort_refund (struct TALER_MERCHANT_PostOrdersAbortHandle *poah,
123 : struct TALER_MERCHANT_PostOrdersAbortResponse *ar,
124 : const json_t *json)
125 : {
126 : const json_t *refunds;
127 : unsigned int num_refunds;
128 : struct GNUNET_JSON_Specification spec[] = {
129 2 : GNUNET_JSON_spec_array_const ("refunds",
130 : &refunds),
131 2 : GNUNET_JSON_spec_end ()
132 : };
133 :
134 2 : if (GNUNET_OK !=
135 2 : GNUNET_JSON_parse (json,
136 : spec,
137 : NULL, NULL))
138 : {
139 0 : GNUNET_break_op (0);
140 0 : return GNUNET_SYSERR;
141 : }
142 2 : num_refunds = (unsigned int) json_array_size (refunds);
143 2 : if ( (json_array_size (refunds) != (size_t) num_refunds) ||
144 2 : (num_refunds != poah->num_coins) )
145 : {
146 0 : GNUNET_break (0);
147 0 : return GNUNET_SYSERR;
148 : }
149 :
150 2 : {
151 2 : struct TALER_MERCHANT_PostOrdersAbortedCoin res[GNUNET_NZL (num_refunds)];
152 :
153 2 : memset (res,
154 : 0,
155 : sizeof (res));
156 4 : for (unsigned int i = 0; i<num_refunds; i++)
157 : {
158 2 : json_t *refund = json_array_get (refunds, i);
159 2 : struct TALER_MERCHANT_PostOrdersAbortedCoin *ac = &res[i];
160 : const char *type;
161 : struct GNUNET_JSON_Specification spec_type[] = {
162 2 : GNUNET_JSON_spec_string ("type",
163 : &type),
164 2 : GNUNET_JSON_spec_end ()
165 : };
166 :
167 2 : ac->coin_pub = poah->coins[i].coin_pub;
168 2 : if (GNUNET_OK !=
169 2 : GNUNET_JSON_parse (refund,
170 : spec_type,
171 : NULL, NULL))
172 : {
173 0 : GNUNET_break_op (0);
174 0 : return GNUNET_SYSERR;
175 : }
176 2 : if (0 == strcmp (type,
177 : "undeposited"))
178 : {
179 : /* An "undeposited" entry (MerchantAbortPayRefundUndepositedStatus)
180 : carries no exchange_status and no refund confirmation to verify. */
181 0 : ac->type = TALER_MERCHANT_POAC_UNDEPOSITED;
182 2 : continue;
183 : }
184 2 : if (0 == strcmp (type,
185 : "success"))
186 2 : {
187 : struct GNUNET_JSON_Specification spec_detail[] = {
188 2 : GNUNET_JSON_spec_uint32 ("exchange_status",
189 : &ac->details.success.exchange_status),
190 2 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
191 : &ac->details.success.exchange_sig),
192 2 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
193 : &ac->details.success.exchange_pub),
194 2 : GNUNET_JSON_spec_end ()
195 : };
196 :
197 2 : ac->type = TALER_MERCHANT_POAC_SUCCESS;
198 2 : if (GNUNET_OK !=
199 2 : GNUNET_JSON_parse (refund,
200 : spec_detail,
201 : NULL, NULL))
202 : {
203 0 : GNUNET_break_op (0);
204 0 : return GNUNET_SYSERR;
205 : }
206 2 : if (MHD_HTTP_OK != ac->details.success.exchange_status)
207 : {
208 0 : GNUNET_break_op (0);
209 0 : return GNUNET_SYSERR;
210 : }
211 :
212 2 : if (GNUNET_OK !=
213 2 : TALER_exchange_online_refund_confirmation_verify (
214 2 : &poah->h_contract,
215 2 : &poah->coins[i].coin_pub,
216 2 : &poah->merchant_pub,
217 : 0, /* transaction id */
218 2 : &poah->coins[i].amount_with_fee,
219 2 : &ac->details.success.exchange_pub,
220 2 : &ac->details.success.exchange_sig))
221 : {
222 0 : GNUNET_break_op (0);
223 0 : return GNUNET_SYSERR;
224 : }
225 2 : continue;
226 : }
227 0 : if (0 == strcmp (type,
228 : "failure"))
229 0 : {
230 : struct GNUNET_JSON_Specification spec_detail[] = {
231 0 : GNUNET_JSON_spec_uint32 ("exchange_status",
232 : &ac->details.failure.exchange_status),
233 0 : TALER_JSON_spec_ec ("exchange_code",
234 : &ac->details.failure.exchange_code),
235 0 : GNUNET_JSON_spec_mark_optional (
236 : GNUNET_JSON_spec_object_const ("exchange_reply",
237 : &ac->details.failure.exchange_reply),
238 : NULL),
239 0 : GNUNET_JSON_spec_end ()
240 : };
241 :
242 0 : ac->type = TALER_MERCHANT_POAC_FAILURE;
243 0 : if (GNUNET_OK !=
244 0 : GNUNET_JSON_parse (refund,
245 : spec_detail,
246 : NULL, NULL))
247 : {
248 0 : GNUNET_break_op (0);
249 0 : return GNUNET_SYSERR;
250 : }
251 0 : continue;
252 : }
253 0 : GNUNET_break_op (0);
254 0 : return GNUNET_SYSERR;
255 : }
256 2 : switch (ar->hr.http_status)
257 : {
258 2 : case MHD_HTTP_OK:
259 2 : ar->details.ok.num_aborts = num_refunds;
260 2 : ar->details.ok.aborts = res;
261 2 : break;
262 0 : case MHD_HTTP_BAD_GATEWAY:
263 0 : ar->details.bad_gateway.num_aborts = num_refunds;
264 0 : ar->details.bad_gateway.aborts = res;
265 0 : break;
266 0 : default:
267 0 : GNUNET_assert (0);
268 : }
269 2 : poah->cb (poah->cb_cls,
270 : ar);
271 2 : poah->cb = NULL;
272 : }
273 2 : return GNUNET_OK;
274 : }
275 :
276 :
277 : /**
278 : * Function called when we're done processing the
279 : * HTTP POST /orders/$ID/abort request.
280 : *
281 : * @param cls the `struct TALER_MERCHANT_PostOrdersAbortHandle`
282 : * @param response_code HTTP response code, 0 on error
283 : * @param response response body, NULL if not in JSON
284 : */
285 : static void
286 2 : handle_abort_finished (void *cls,
287 : long response_code,
288 : const void *response)
289 : {
290 2 : struct TALER_MERCHANT_PostOrdersAbortHandle *poah = cls;
291 2 : const json_t *json = response;
292 2 : struct TALER_MERCHANT_PostOrdersAbortResponse ar = {
293 2 : .hr.http_status = (unsigned int) response_code,
294 : .hr.reply = json
295 : };
296 :
297 2 : poah->job = NULL;
298 2 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
299 : "POST /orders/$ID/abort completed with response code %u\n",
300 : (unsigned int) response_code);
301 2 : switch (response_code)
302 : {
303 0 : case 0:
304 0 : ar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
305 0 : break;
306 2 : case MHD_HTTP_OK:
307 2 : if (GNUNET_OK ==
308 2 : check_abort_refund (poah,
309 : &ar,
310 : json))
311 : {
312 2 : TALER_MERCHANT_post_orders_abort_cancel (poah);
313 2 : return;
314 : }
315 0 : ar.hr.http_status = 0;
316 0 : ar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
317 0 : break;
318 0 : case MHD_HTTP_BAD_REQUEST:
319 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
320 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
321 0 : break;
322 0 : case MHD_HTTP_FORBIDDEN:
323 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
324 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
325 0 : break;
326 0 : case MHD_HTTP_NOT_FOUND:
327 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
328 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
329 0 : break;
330 0 : case MHD_HTTP_REQUEST_TIMEOUT:
331 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
332 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
333 0 : break;
334 0 : case MHD_HTTP_PRECONDITION_FAILED:
335 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
336 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
337 0 : break;
338 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
339 0 : ar.hr.ec = TALER_JSON_get_error_code (json);
340 0 : ar.hr.hint = TALER_JSON_get_error_hint (json);
341 0 : break;
342 0 : case MHD_HTTP_BAD_GATEWAY:
343 0 : if (GNUNET_OK ==
344 0 : check_abort_refund (poah,
345 : &ar,
346 : json))
347 : {
348 0 : TALER_MERCHANT_post_orders_abort_cancel (poah);
349 0 : return;
350 : }
351 0 : ar.hr.http_status = 0;
352 0 : ar.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
353 0 : break;
354 0 : case MHD_HTTP_GATEWAY_TIMEOUT:
355 0 : TALER_MERCHANT_parse_error_details_ (json,
356 : response_code,
357 : &ar.hr);
358 0 : ar.details.gateway_timeout.exchange_ec = ar.hr.exchange_code;
359 : ar.details.gateway_timeout.exchange_http_status
360 0 : = ar.hr.exchange_http_status;
361 0 : break;
362 0 : default:
363 0 : TALER_MERCHANT_parse_error_details_ (json,
364 : response_code,
365 : &ar.hr);
366 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
367 : "Unexpected response code %u/%d\n",
368 : (unsigned int) response_code,
369 : (int) ar.hr.ec);
370 0 : GNUNET_break_op (0);
371 0 : break;
372 : }
373 0 : poah->cb (poah->cb_cls,
374 : &ar);
375 0 : TALER_MERCHANT_post_orders_abort_cancel (poah);
376 : }
377 :
378 :
379 : struct TALER_MERCHANT_PostOrdersAbortHandle *
380 2 : TALER_MERCHANT_post_orders_abort_create (
381 : struct GNUNET_CURL_Context *ctx,
382 : const char *url,
383 : const char *order_id,
384 : const struct TALER_MerchantPublicKeyP *merchant_pub,
385 : const struct TALER_PrivateContractHashP *h_contract,
386 : unsigned int num_coins,
387 : const struct TALER_MERCHANT_PostOrdersAbortCoin coins[static num_coins])
388 2 : {
389 : struct TALER_MERCHANT_PostOrdersAbortHandle *poah;
390 :
391 2 : if (MAX_REFUNDS < num_coins)
392 : {
393 0 : GNUNET_break (0);
394 0 : return NULL;
395 : }
396 2 : poah = GNUNET_new (struct TALER_MERCHANT_PostOrdersAbortHandle);
397 2 : poah->ctx = ctx;
398 2 : poah->base_url = GNUNET_strdup (url);
399 2 : poah->order_id = GNUNET_strdup (order_id);
400 2 : poah->merchant_pub = *merchant_pub;
401 2 : poah->h_contract = *h_contract;
402 2 : poah->num_coins = num_coins;
403 2 : poah->coins = GNUNET_new_array (num_coins,
404 : struct TALER_MERCHANT_PostOrdersAbortCoin);
405 2 : GNUNET_memcpy (
406 : poah->coins,
407 : coins,
408 : num_coins * sizeof (struct TALER_MERCHANT_PostOrdersAbortCoin));
409 4 : for (unsigned int i = 0; i<num_coins; i++)
410 2 : poah->coins[i].exchange_url
411 2 : = GNUNET_strdup (poah->coins[i].exchange_url);
412 2 : return poah;
413 : }
414 :
415 :
416 : enum TALER_ErrorCode
417 2 : TALER_MERCHANT_post_orders_abort_start (
418 : struct TALER_MERCHANT_PostOrdersAbortHandle *poah,
419 : TALER_MERCHANT_PostOrdersAbortCallback cb,
420 : TALER_MERCHANT_POST_ORDERS_ABORT_RESULT_CLOSURE *cb_cls)
421 : {
422 : json_t *abort_obj;
423 : json_t *j_coins;
424 : CURL *eh;
425 :
426 2 : poah->cb = cb;
427 2 : poah->cb_cls = cb_cls;
428 : {
429 : char *path;
430 :
431 2 : GNUNET_asprintf (&path,
432 : "orders/%s/abort",
433 : poah->order_id);
434 2 : poah->url = TALER_url_join (poah->base_url,
435 : path,
436 : NULL);
437 2 : GNUNET_free (path);
438 : }
439 2 : if (NULL == poah->url)
440 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
441 2 : j_coins = json_array ();
442 2 : if (NULL == j_coins)
443 : {
444 0 : GNUNET_break (0);
445 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
446 : }
447 4 : for (unsigned int i = 0; i<poah->num_coins; i++)
448 : {
449 2 : const struct TALER_MERCHANT_PostOrdersAbortCoin *ac = &poah->coins[i];
450 : json_t *j_coin;
451 :
452 2 : j_coin = GNUNET_JSON_PACK (
453 : GNUNET_JSON_pack_data_auto ("coin_pub",
454 : &ac->coin_pub),
455 : GNUNET_JSON_pack_string ("exchange_url",
456 : ac->exchange_url));
457 2 : if (0 !=
458 2 : json_array_append_new (j_coins,
459 : j_coin))
460 : {
461 0 : GNUNET_break (0);
462 0 : json_decref (j_coins);
463 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
464 : }
465 : }
466 2 : abort_obj = GNUNET_JSON_PACK (
467 : GNUNET_JSON_pack_array_steal ("coins",
468 : j_coins),
469 : GNUNET_JSON_pack_data_auto ("h_contract",
470 : &poah->h_contract));
471 2 : eh = TALER_MERCHANT_curl_easy_get_ (poah->url);
472 4 : if ( (NULL == eh) ||
473 : (GNUNET_OK !=
474 2 : TALER_curl_easy_post (&poah->post_ctx,
475 : eh,
476 : abort_obj)) )
477 : {
478 0 : GNUNET_break (0);
479 0 : json_decref (abort_obj);
480 0 : if (NULL != eh)
481 0 : curl_easy_cleanup (eh);
482 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
483 : }
484 2 : json_decref (abort_obj);
485 4 : poah->job = GNUNET_CURL_job_add2 (poah->ctx,
486 : eh,
487 2 : poah->post_ctx.headers,
488 : &handle_abort_finished,
489 : poah);
490 2 : if (NULL == poah->job)
491 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
492 2 : return TALER_EC_NONE;
493 : }
494 :
495 :
496 : void
497 2 : TALER_MERCHANT_post_orders_abort_cancel (
498 : struct TALER_MERCHANT_PostOrdersAbortHandle *poah)
499 : {
500 2 : if (NULL != poah->job)
501 : {
502 0 : GNUNET_CURL_job_cancel (poah->job);
503 0 : poah->job = NULL;
504 : }
505 2 : TALER_curl_easy_post_finished (&poah->post_ctx);
506 4 : for (unsigned int i = 0; i < poah->num_coins; i++)
507 2 : GNUNET_free_nz ((char *) poah->coins[i].exchange_url);
508 2 : GNUNET_free (poah->coins);
509 2 : GNUNET_free (poah->order_id);
510 2 : GNUNET_free (poah->url);
511 2 : GNUNET_free (poah->base_url);
512 2 : GNUNET_free (poah);
513 2 : }
514 :
515 :
516 : /* end of merchant_api_post-orders-ORDER_ID-abort-new.c */
|