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 it under the
6 : terms of the GNU 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 General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see
15 : <http://www.gnu.org/licenses/>
16 : */
17 : /**
18 : * @file lib/exchange_api_post-coins-COIN_PUB-refund.c
19 : * @brief Implementation of the /refund request of the exchange's HTTP API
20 : * @author Christian Grothoff
21 : */
22 : #include "taler/platform.h"
23 : #include <jansson.h>
24 : #include <microhttpd.h> /* just for HTTP status codes */
25 : #include <gnunet/gnunet_util_lib.h>
26 : #include <gnunet/gnunet_json_lib.h>
27 : #include <gnunet/gnunet_curl_lib.h>
28 : #include "taler/taler_json_lib.h"
29 : #include "taler/taler_exchange_service.h"
30 : #include "taler/taler-exchange/post-coins-COIN_PUB-refund.h"
31 : #include "exchange_api_handle.h"
32 : #include "taler/taler_signatures.h"
33 : #include "exchange_api_curl_defaults.h"
34 :
35 :
36 : /**
37 : * @brief A POST /coins/$COIN_PUB/refund Handle
38 : */
39 : struct TALER_EXCHANGE_PostCoinsRefundHandle
40 : {
41 :
42 : /**
43 : * The keys of the exchange this request handle will use
44 : */
45 : struct TALER_EXCHANGE_Keys *keys;
46 :
47 : /**
48 : * The exchange base URL.
49 : */
50 : char *base_url;
51 :
52 : /**
53 : * The full URL for this request, set during _start.
54 : */
55 : char *url;
56 :
57 : /**
58 : * Reference to the execution context.
59 : */
60 : struct GNUNET_CURL_Context *ctx;
61 :
62 : /**
63 : * Context for #TEH_curl_easy_post(). Keeps the data that must
64 : * persist for Curl to make the upload.
65 : */
66 : struct TALER_CURL_PostContext post_ctx;
67 :
68 : /**
69 : * Handle for the request.
70 : */
71 : struct GNUNET_CURL_Job *job;
72 :
73 : /**
74 : * Function to call with the result.
75 : */
76 : TALER_EXCHANGE_PostCoinsRefundCallback cb;
77 :
78 : /**
79 : * Closure for @e cb.
80 : */
81 : TALER_EXCHANGE_POST_COINS_REFUND_RESULT_CLOSURE *cb_cls;
82 :
83 : /**
84 : * Hash over the proposal data to identify the contract
85 : * which is being refunded.
86 : */
87 : struct TALER_PrivateContractHashP h_contract_terms;
88 :
89 : /**
90 : * The coin's public key. This is the value that must have been
91 : * signed (blindly) by the Exchange.
92 : */
93 : struct TALER_CoinSpendPublicKeyP coin_pub;
94 :
95 : /**
96 : * The Merchant's public key.
97 : */
98 : struct TALER_MerchantPublicKeyP merchant_pub;
99 :
100 : /**
101 : * The merchant's private key (for signing).
102 : */
103 : struct TALER_MerchantPrivateKeyP merchant_priv;
104 :
105 : /**
106 : * Merchant-generated transaction ID for the refund.
107 : */
108 : uint64_t rtransaction_id;
109 :
110 : /**
111 : * Amount to be refunded.
112 : */
113 : struct TALER_Amount refund_amount;
114 :
115 : };
116 :
117 :
118 : /**
119 : * Verify that the signature on the "200 OK" response
120 : * from the exchange is valid.
121 : *
122 : * @param[in,out] rh refund handle (refund fee added)
123 : * @param json json reply with the signature
124 : * @param[out] exchange_pub set to the exchange's public key
125 : * @param[out] exchange_sig set to the exchange's signature
126 : * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
127 : */
128 : static enum GNUNET_GenericReturnValue
129 10 : verify_refund_signature_ok (struct TALER_EXCHANGE_PostCoinsRefundHandle *rh,
130 : const json_t *json,
131 : struct TALER_ExchangePublicKeyP *exchange_pub,
132 : struct TALER_ExchangeSignatureP *exchange_sig)
133 : {
134 : struct GNUNET_JSON_Specification spec[] = {
135 10 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
136 : exchange_sig),
137 10 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
138 : exchange_pub),
139 10 : GNUNET_JSON_spec_end ()
140 : };
141 :
142 10 : if (GNUNET_OK !=
143 10 : GNUNET_JSON_parse (json,
144 : spec,
145 : NULL, NULL))
146 : {
147 0 : GNUNET_break_op (0);
148 0 : return GNUNET_SYSERR;
149 : }
150 10 : if (GNUNET_OK !=
151 10 : TALER_EXCHANGE_test_signing_key (rh->keys,
152 : exchange_pub))
153 : {
154 0 : GNUNET_break_op (0);
155 0 : return GNUNET_SYSERR;
156 : }
157 10 : if (GNUNET_OK !=
158 10 : TALER_exchange_online_refund_confirmation_verify (
159 10 : &rh->h_contract_terms,
160 10 : &rh->coin_pub,
161 10 : &rh->merchant_pub,
162 : rh->rtransaction_id,
163 10 : &rh->refund_amount,
164 : exchange_pub,
165 : exchange_sig))
166 : {
167 0 : GNUNET_break_op (0);
168 0 : return GNUNET_SYSERR;
169 : }
170 10 : return GNUNET_OK;
171 : }
172 :
173 :
174 : /**
175 : * Verify that the information on the "412 Dependency Failed" response
176 : * from the exchange is valid.
177 : *
178 : * @param[in,out] rh refund handle
179 : * @param json json reply with the signature
180 : * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR if not
181 : */
182 : static enum GNUNET_GenericReturnValue
183 0 : verify_failed_dependency_ok (struct TALER_EXCHANGE_PostCoinsRefundHandle *rh,
184 : const json_t *json)
185 : {
186 : const json_t *h;
187 : json_t *e;
188 : struct GNUNET_JSON_Specification spec[] = {
189 0 : GNUNET_JSON_spec_array_const ("history",
190 : &h),
191 0 : GNUNET_JSON_spec_end ()
192 : };
193 :
194 0 : if (GNUNET_OK !=
195 0 : GNUNET_JSON_parse (json,
196 : spec,
197 : NULL, NULL))
198 : {
199 0 : GNUNET_break_op (0);
200 0 : return GNUNET_SYSERR;
201 : }
202 0 : if (1 != json_array_size (h))
203 : {
204 0 : GNUNET_break_op (0);
205 0 : return GNUNET_SYSERR;
206 : }
207 0 : e = json_array_get (h, 0);
208 : {
209 : struct TALER_Amount amount;
210 : const char *type;
211 : struct TALER_MerchantSignatureP sig;
212 : struct TALER_Amount refund_fee;
213 : struct TALER_PrivateContractHashP h_contract_terms;
214 : uint64_t rtransaction_id;
215 : struct TALER_MerchantPublicKeyP merchant_pub;
216 : struct GNUNET_JSON_Specification ispec[] = {
217 0 : TALER_JSON_spec_amount_any ("amount",
218 : &amount),
219 0 : GNUNET_JSON_spec_string ("type",
220 : &type),
221 0 : TALER_JSON_spec_amount_any ("refund_fee",
222 : &refund_fee),
223 0 : GNUNET_JSON_spec_fixed_auto ("merchant_sig",
224 : &sig),
225 0 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
226 : &h_contract_terms),
227 0 : GNUNET_JSON_spec_fixed_auto ("merchant_pub",
228 : &merchant_pub),
229 0 : GNUNET_JSON_spec_uint64 ("rtransaction_id",
230 : &rtransaction_id),
231 0 : GNUNET_JSON_spec_end ()
232 : };
233 :
234 0 : if (GNUNET_OK !=
235 0 : GNUNET_JSON_parse (e,
236 : ispec,
237 : NULL, NULL))
238 : {
239 0 : GNUNET_break_op (0);
240 0 : return GNUNET_SYSERR;
241 : }
242 0 : if (GNUNET_OK !=
243 0 : TALER_merchant_refund_verify (&rh->coin_pub,
244 : &h_contract_terms,
245 : rtransaction_id,
246 : &amount,
247 : &merchant_pub,
248 : &sig))
249 : {
250 0 : GNUNET_break_op (0);
251 0 : return GNUNET_SYSERR;
252 : }
253 0 : if ( (rtransaction_id != rh->rtransaction_id) ||
254 0 : (0 != GNUNET_memcmp (&rh->h_contract_terms,
255 0 : &h_contract_terms)) ||
256 0 : (0 != GNUNET_memcmp (&rh->merchant_pub,
257 0 : &merchant_pub)) ||
258 0 : (0 == TALER_amount_cmp (&rh->refund_amount,
259 : &amount)) )
260 : {
261 0 : GNUNET_break_op (0);
262 0 : return GNUNET_SYSERR;
263 : }
264 : }
265 0 : return GNUNET_OK;
266 : }
267 :
268 :
269 : /**
270 : * Function called when we're done processing the
271 : * HTTP /refund request.
272 : *
273 : * @param cls the `struct TALER_EXCHANGE_PostCoinsRefundHandle`
274 : * @param response_code HTTP response code, 0 on error
275 : * @param response parsed JSON result, NULL on error
276 : */
277 : static void
278 14 : handle_refund_finished (void *cls,
279 : long response_code,
280 : const void *response)
281 : {
282 14 : struct TALER_EXCHANGE_PostCoinsRefundHandle *rh = cls;
283 14 : const json_t *j = response;
284 14 : struct TALER_EXCHANGE_PostCoinsRefundResponse rr = {
285 : .hr.reply = j,
286 14 : .hr.http_status = (unsigned int) response_code
287 : };
288 :
289 14 : rh->job = NULL;
290 14 : switch (response_code)
291 : {
292 0 : case 0:
293 0 : rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
294 0 : break;
295 10 : case MHD_HTTP_OK:
296 10 : if (GNUNET_OK !=
297 10 : verify_refund_signature_ok (rh,
298 : j,
299 : &rr.details.ok.exchange_pub,
300 : &rr.details.ok.exchange_sig))
301 : {
302 0 : GNUNET_break_op (0);
303 0 : rr.hr.http_status = 0;
304 0 : rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE;
305 : }
306 10 : break;
307 0 : case MHD_HTTP_BAD_REQUEST:
308 : /* This should never happen, either us or the exchange is buggy
309 : (or API version conflict); also can happen if the currency
310 : differs (which we should obviously never support).
311 : Just pass JSON reply to the application */
312 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
313 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
314 0 : break;
315 0 : case MHD_HTTP_FORBIDDEN:
316 : /* Nothing really to verify, exchange says one of the signatures is
317 : invalid; as we checked them, this should never happen, we
318 : should pass the JSON reply to the application */
319 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
320 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
321 0 : break;
322 0 : case MHD_HTTP_NOT_FOUND:
323 : /* Nothing really to verify, this should never
324 : happen, we should pass the JSON reply to the application */
325 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
326 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
327 0 : break;
328 2 : case MHD_HTTP_CONFLICT:
329 : /* Requested total refunds exceed deposited amount */
330 2 : rr.hr.ec = TALER_JSON_get_error_code (j);
331 2 : rr.hr.hint = TALER_JSON_get_error_hint (j);
332 2 : break;
333 2 : case MHD_HTTP_GONE:
334 : /* Kind of normal: the money was already sent to the merchant
335 : (it was too late for the refund). */
336 2 : rr.hr.ec = TALER_JSON_get_error_code (j);
337 2 : rr.hr.hint = TALER_JSON_get_error_hint (j);
338 2 : break;
339 0 : case MHD_HTTP_FAILED_DEPENDENCY:
340 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
341 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
342 0 : break;
343 0 : case MHD_HTTP_PRECONDITION_FAILED:
344 0 : if (GNUNET_OK !=
345 0 : verify_failed_dependency_ok (rh,
346 : j))
347 : {
348 0 : GNUNET_break (0);
349 0 : rr.hr.http_status = 0;
350 0 : rr.hr.ec = TALER_EC_EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE;
351 0 : rr.hr.hint = "failed precondition proof returned by exchange is invalid";
352 0 : break;
353 : }
354 : /* Two different refund requests were made about the same deposit, but
355 : carrying identical refund transaction ids. */
356 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
357 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
358 0 : break;
359 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
360 : /* Server had an internal issue; we should retry, but this API
361 : leaves this to the application */
362 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
363 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
364 0 : break;
365 0 : default:
366 : /* unexpected response code */
367 0 : GNUNET_break_op (0);
368 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
369 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
370 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
371 : "Unexpected response code %u/%d for exchange refund\n",
372 : (unsigned int) response_code,
373 : rr.hr.ec);
374 0 : break;
375 : }
376 14 : if (NULL != rh->cb)
377 : {
378 14 : rh->cb (rh->cb_cls,
379 : &rr);
380 14 : rh->cb = NULL;
381 : }
382 14 : TALER_EXCHANGE_post_coins_refund_cancel (rh);
383 14 : }
384 :
385 :
386 : struct TALER_EXCHANGE_PostCoinsRefundHandle *
387 14 : TALER_EXCHANGE_post_coins_refund_create (
388 : struct GNUNET_CURL_Context *ctx,
389 : const char *url,
390 : struct TALER_EXCHANGE_Keys *keys,
391 : const struct TALER_Amount *amount,
392 : const struct TALER_PrivateContractHashP *h_contract_terms,
393 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
394 : uint64_t rtransaction_id,
395 : const struct TALER_MerchantPrivateKeyP *merchant_priv)
396 : {
397 : struct TALER_EXCHANGE_PostCoinsRefundHandle *rh;
398 :
399 14 : rh = GNUNET_new (struct TALER_EXCHANGE_PostCoinsRefundHandle);
400 14 : rh->ctx = ctx;
401 14 : rh->base_url = GNUNET_strdup (url);
402 14 : rh->keys = TALER_EXCHANGE_keys_incref (keys);
403 14 : rh->refund_amount = *amount;
404 14 : rh->h_contract_terms = *h_contract_terms;
405 14 : rh->coin_pub = *coin_pub;
406 14 : rh->rtransaction_id = rtransaction_id;
407 14 : rh->merchant_priv = *merchant_priv;
408 14 : GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv,
409 : &rh->merchant_pub.eddsa_pub);
410 14 : return rh;
411 : }
412 :
413 :
414 : enum TALER_ErrorCode
415 14 : TALER_EXCHANGE_post_coins_refund_start (
416 : struct TALER_EXCHANGE_PostCoinsRefundHandle *rh,
417 : TALER_EXCHANGE_PostCoinsRefundCallback cb,
418 : TALER_EXCHANGE_POST_COINS_REFUND_RESULT_CLOSURE *cb_cls)
419 : {
420 : struct TALER_MerchantSignatureP merchant_sig;
421 : json_t *refund_obj;
422 : CURL *eh;
423 : char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 32];
424 :
425 14 : rh->cb = cb;
426 14 : rh->cb_cls = cb_cls;
427 14 : TALER_merchant_refund_sign (&rh->coin_pub,
428 14 : &rh->h_contract_terms,
429 : rh->rtransaction_id,
430 14 : &rh->refund_amount,
431 14 : &rh->merchant_priv,
432 : &merchant_sig);
433 : {
434 : char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
435 : char *end;
436 :
437 14 : end = GNUNET_STRINGS_data_to_string (
438 14 : &rh->coin_pub,
439 : sizeof (struct TALER_CoinSpendPublicKeyP),
440 : pub_str,
441 : sizeof (pub_str));
442 14 : *end = '\0';
443 14 : GNUNET_snprintf (arg_str,
444 : sizeof (arg_str),
445 : "coins/%s/refund",
446 : pub_str);
447 : }
448 14 : refund_obj = GNUNET_JSON_PACK (
449 : TALER_JSON_pack_amount ("refund_amount",
450 : &rh->refund_amount),
451 : GNUNET_JSON_pack_data_auto ("h_contract_terms",
452 : &rh->h_contract_terms),
453 : GNUNET_JSON_pack_uint64 ("rtransaction_id",
454 : rh->rtransaction_id),
455 : GNUNET_JSON_pack_data_auto ("merchant_pub",
456 : &rh->merchant_pub),
457 : GNUNET_JSON_pack_data_auto ("merchant_sig",
458 : &merchant_sig));
459 14 : rh->url = TALER_url_join (rh->base_url,
460 : arg_str,
461 : NULL);
462 14 : if (NULL == rh->url)
463 : {
464 0 : json_decref (refund_obj);
465 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
466 : }
467 14 : eh = TALER_EXCHANGE_curl_easy_get_ (rh->url);
468 28 : if ( (NULL == eh) ||
469 : (GNUNET_OK !=
470 14 : TALER_curl_easy_post (&rh->post_ctx,
471 : eh,
472 : refund_obj)) )
473 : {
474 0 : GNUNET_break (0);
475 0 : if (NULL != eh)
476 0 : curl_easy_cleanup (eh);
477 0 : json_decref (refund_obj);
478 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
479 : }
480 14 : json_decref (refund_obj);
481 14 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
482 : "URL for refund: `%s'\n",
483 : rh->url);
484 28 : rh->job = GNUNET_CURL_job_add2 (rh->ctx,
485 : eh,
486 14 : rh->post_ctx.headers,
487 : &handle_refund_finished,
488 : rh);
489 14 : if (NULL == rh->job)
490 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
491 14 : return TALER_EC_NONE;
492 : }
493 :
494 :
495 : void
496 14 : TALER_EXCHANGE_post_coins_refund_cancel (
497 : struct TALER_EXCHANGE_PostCoinsRefundHandle *rh)
498 : {
499 14 : if (NULL != rh->job)
500 : {
501 0 : GNUNET_CURL_job_cancel (rh->job);
502 0 : rh->job = NULL;
503 : }
504 14 : GNUNET_free (rh->url);
505 14 : GNUNET_free (rh->base_url);
506 14 : TALER_curl_easy_post_finished (&rh->post_ctx);
507 14 : TALER_EXCHANGE_keys_decref (rh->keys);
508 14 : GNUNET_free (rh);
509 14 : }
510 :
511 :
512 : /* end of exchange_api_post-coins-COIN_PUB-refund.c */
|