Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2017-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 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 lib/exchange_api_post-recoup-withdraw.c
18 : * @brief Implementation of the /recoup-withdraw request of the exchange's HTTP API
19 : * @author Christian Grothoff
20 : * @author Özgür Kesim
21 : */
22 : #include <jansson.h>
23 : #include <microhttpd.h> /* just for HTTP status codes */
24 : #include <gnunet/gnunet_util_lib.h>
25 : #include <gnunet/gnunet_json_lib.h>
26 : #include <gnunet/gnunet_curl_lib.h>
27 : #include "taler/taler_json_lib.h"
28 : #include "exchange_api_common.h"
29 : #include "exchange_api_handle.h"
30 : #include "taler/taler_signatures.h"
31 : #include "exchange_api_curl_defaults.h"
32 :
33 :
34 : /**
35 : * @brief A /recoup-withdraw handle
36 : */
37 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle
38 : {
39 :
40 : /**
41 : * The base URL for this request.
42 : */
43 : char *base_url;
44 :
45 : /**
46 : * The full URL for this request, set during _start.
47 : */
48 : char *url;
49 :
50 : /**
51 : * Minor context that holds body and headers.
52 : */
53 : struct TALER_CURL_PostContext post_ctx;
54 :
55 : /**
56 : * Handle for the request.
57 : */
58 : struct GNUNET_CURL_Job *job;
59 :
60 : /**
61 : * Function to call with the result.
62 : */
63 : TALER_EXCHANGE_PostRecoupWithdrawCallback cb;
64 :
65 : /**
66 : * Closure for @a cb.
67 : */
68 : TALER_EXCHANGE_POST_RECOUP_WITHDRAW_RESULT_CLOSURE *cb_cls;
69 :
70 : /**
71 : * Reference to the execution context.
72 : */
73 : struct GNUNET_CURL_Context *ctx;
74 :
75 : /**
76 : * The keys of the exchange this request handle will use.
77 : */
78 : struct TALER_EXCHANGE_Keys *keys;
79 :
80 : /**
81 : * Reserve the coins were withdrawn from.
82 : */
83 : struct TALER_ReservePublicKeyP reserve_pub;
84 :
85 : /**
86 : * Commitment of the withdraw operation.
87 : */
88 : struct TALER_HashBlindedPlanchetsP planchets_h;
89 :
90 : /**
91 : * Public keys of the recouped coins, in request order.
92 : */
93 : struct TALER_CoinSpendPublicKeyP *recouped_pubs;
94 :
95 : /**
96 : * Denomination and age commitment of the recouped coins,
97 : * parallel to @e recouped_pubs.
98 : */
99 : struct TALER_EXCHANGE_RecoupedCoinInfo_ *recouped_infos;
100 :
101 : /**
102 : * Number of entries in @e recouped_pubs.
103 : */
104 : size_t num_recouped;
105 :
106 : /**
107 : * Pre-built request body.
108 : */
109 : json_t *body;
110 :
111 : };
112 :
113 :
114 : /**
115 : * Parse and verify a successful response. If it is valid,
116 : * call the callback.
117 : *
118 : * @param prwh request handle
119 : * @param json json reply with the signature
120 : * @return #GNUNET_OK if the response is valid and we called the callback;
121 : * #GNUNET_SYSERR if not (callback must still be called)
122 : */
123 : static enum GNUNET_GenericReturnValue
124 18 : process_ok_response (
125 : const struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh,
126 : const json_t *json)
127 : {
128 18 : struct TALER_EXCHANGE_PostRecoupWithdrawResponse rr = {
129 : .hr.reply = json,
130 : .hr.http_status = MHD_HTTP_OK
131 : };
132 : const json_t *j_recoups;
133 : struct TALER_RecoupedCoin *recoups;
134 : struct GNUNET_HashCode h_recoups;
135 : struct GNUNET_JSON_Specification spec[] = {
136 18 : GNUNET_JSON_spec_fixed_auto ("reserve_pub",
137 : &rr.details.ok.reserve_pub),
138 18 : GNUNET_JSON_spec_fixed_auto ("planchets_h",
139 : &rr.details.ok.planchets_h),
140 18 : GNUNET_JSON_spec_timestamp ("timestamp",
141 : &rr.details.ok.timestamp),
142 18 : TALER_JSON_spec_amount_any ("total_amount",
143 : &rr.details.ok.total_amount),
144 18 : GNUNET_JSON_spec_array_const ("recoups",
145 : &j_recoups),
146 18 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
147 : &rr.details.ok.exchange_sig),
148 18 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
149 : &rr.details.ok.exchange_pub),
150 18 : GNUNET_JSON_spec_end ()
151 : };
152 :
153 18 : if (GNUNET_OK !=
154 18 : GNUNET_JSON_parse (json,
155 : spec,
156 : NULL, NULL))
157 : {
158 0 : GNUNET_break_op (0);
159 0 : return GNUNET_SYSERR;
160 : }
161 18 : if ( (0 !=
162 18 : GNUNET_memcmp (&rr.details.ok.reserve_pub,
163 18 : &prwh->reserve_pub)) ||
164 : (0 !=
165 18 : GNUNET_memcmp (&rr.details.ok.planchets_h,
166 : &prwh->planchets_h)) )
167 : {
168 0 : GNUNET_break_op (0);
169 0 : return GNUNET_SYSERR;
170 : }
171 18 : if (GNUNET_OK !=
172 18 : TALER_EXCHANGE_test_signing_key (prwh->keys,
173 : &rr.details.ok.exchange_pub))
174 : {
175 0 : GNUNET_break_op (0);
176 0 : return GNUNET_SYSERR;
177 : }
178 18 : if (GNUNET_OK !=
179 18 : TALER_EXCHANGE_parse_recoups_ (j_recoups,
180 18 : prwh->num_recouped,
181 18 : prwh->recouped_pubs,
182 : &rr.details.ok.total_amount,
183 : &recoups,
184 : &h_recoups))
185 : {
186 0 : GNUNET_break_op (0);
187 0 : return GNUNET_SYSERR;
188 : }
189 18 : if (GNUNET_OK !=
190 18 : TALER_exchange_online_confirm_recoup_withdraw_batch_verify (
191 : rr.details.ok.timestamp,
192 : &rr.details.ok.reserve_pub,
193 : &rr.details.ok.planchets_h,
194 : &rr.details.ok.total_amount,
195 : &h_recoups,
196 : &rr.details.ok.exchange_pub,
197 : &rr.details.ok.exchange_sig))
198 : {
199 0 : GNUNET_break_op (0);
200 0 : GNUNET_free (recoups);
201 0 : return GNUNET_SYSERR;
202 : }
203 18 : rr.details.ok.num_recoups = prwh->num_recouped;
204 18 : rr.details.ok.recoups = recoups;
205 18 : prwh->cb (prwh->cb_cls,
206 : &rr);
207 18 : GNUNET_free (recoups);
208 18 : return GNUNET_OK;
209 : }
210 :
211 :
212 : /**
213 : * Find the recouped coin @a coin_pub and how we used it.
214 : *
215 : * @param cls a `struct TALER_EXCHANGE_PostRecoupWithdrawHandle *`
216 : * @param coin_pub public key of the coin named in the conflict reply
217 : * @param[out] h_denom_pub set to the hash of the denomination we used
218 : * @param[out] h_age_commitment set to the age commitment hash we used, or NULL
219 : * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours
220 : */
221 : static enum GNUNET_GenericReturnValue
222 1 : recoup_coin_lookup (void *cls,
223 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
224 : const struct TALER_DenominationHashP **h_denom_pub,
225 : const struct TALER_AgeCommitmentHashP **h_age_commitment)
226 : {
227 1 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh = cls;
228 :
229 1 : for (size_t i = 0; i < prwh->num_recouped; i++)
230 : {
231 1 : if (0 != GNUNET_memcmp (coin_pub,
232 : &prwh->recouped_pubs[i]))
233 0 : continue;
234 1 : *h_denom_pub = &prwh->recouped_infos[i].h_denom_pub;
235 2 : *h_age_commitment = prwh->recouped_infos[i].have_age
236 0 : ? &prwh->recouped_infos[i].h_age_commitment
237 1 : : NULL;
238 1 : return GNUNET_OK;
239 : }
240 0 : return GNUNET_NO;
241 : }
242 :
243 :
244 : /**
245 : * Function called when we're done processing the
246 : * HTTP /recoup-withdraw request.
247 : *
248 : * @param cls the `struct TALER_EXCHANGE_PostRecoupWithdrawHandle`
249 : * @param response_code HTTP response code, 0 on error
250 : * @param response parsed JSON result, NULL on error
251 : */
252 : static void
253 22 : handle_recoup_withdraw_finished (void *cls,
254 : long response_code,
255 : const void *response)
256 : {
257 22 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh = cls;
258 22 : const json_t *j = response;
259 22 : struct TALER_EXCHANGE_PostRecoupWithdrawResponse rr = {
260 : .hr.reply = j,
261 22 : .hr.http_status = (unsigned int) response_code
262 : };
263 :
264 22 : prwh->job = NULL;
265 22 : switch (response_code)
266 : {
267 0 : case 0:
268 0 : rr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
269 0 : break;
270 18 : case MHD_HTTP_OK:
271 18 : if (GNUNET_OK !=
272 18 : process_ok_response (prwh,
273 : j))
274 : {
275 0 : GNUNET_break_op (0);
276 0 : rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
277 0 : rr.hr.http_status = 0;
278 0 : break;
279 : }
280 18 : TALER_EXCHANGE_post_recoup_withdraw_cancel (prwh);
281 18 : return;
282 0 : case MHD_HTTP_BAD_REQUEST:
283 : /* This should never happen, either us or the exchange is buggy
284 : (or API version conflict); just pass JSON reply to the application */
285 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
286 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
287 0 : break;
288 0 : case MHD_HTTP_FORBIDDEN:
289 : /* Nothing really to verify, exchange says one of the signatures is
290 : invalid; as we checked them, this should never happen, we
291 : should pass the JSON reply to the application */
292 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
293 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
294 0 : break;
295 0 : case MHD_HTTP_NOT_FOUND:
296 : /* Exchange does not know the denomination or the withdraw
297 : operation; pass the JSON reply to the application */
298 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
299 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
300 0 : break;
301 4 : case MHD_HTTP_CONFLICT:
302 4 : rr.hr.ec = TALER_JSON_get_error_code (j);
303 4 : rr.hr.hint = TALER_JSON_get_error_hint (j);
304 4 : switch (rr.hr.ec)
305 : {
306 1 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
307 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH:
308 1 : if (GNUNET_OK !=
309 1 : TALER_EXCHANGE_check_coin_conflict_ (prwh->keys,
310 : rr.hr.ec,
311 : j,
312 : &recoup_coin_lookup,
313 : prwh,
314 : &rr.details.conflict))
315 : {
316 0 : GNUNET_break_op (0);
317 0 : rr.hr.http_status = 0;
318 0 : rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
319 : }
320 1 : break;
321 3 : default:
322 : /* Commitment mismatch, denomination mismatch or a coin without
323 : residual value; pass the JSON reply to the application */
324 3 : break;
325 : }
326 4 : break;
327 0 : case MHD_HTTP_GONE:
328 : /* Denomination not eligible for recoup (not revoked, or expired) */
329 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
330 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
331 0 : break;
332 0 : case MHD_HTTP_PRECONDITION_FAILED:
333 : /* Denomination not yet valid */
334 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
335 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
336 0 : break;
337 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
338 : /* Server had an internal issue; we should retry, but this API
339 : leaves this to the application */
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_BAD_GATEWAY:
344 : case MHD_HTTP_SERVICE_UNAVAILABLE:
345 : /* Exchange is (temporarily) unable to sign; retry later */
346 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
347 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
348 0 : break;
349 0 : default:
350 : /* unexpected response code */
351 0 : rr.hr.ec = TALER_JSON_get_error_code (j);
352 0 : rr.hr.hint = TALER_JSON_get_error_hint (j);
353 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
354 : "Unexpected response code %u/%d for exchange recoup-withdraw\n",
355 : (unsigned int) response_code,
356 : (int) rr.hr.ec);
357 0 : GNUNET_break (0);
358 0 : break;
359 : }
360 4 : prwh->cb (prwh->cb_cls,
361 : &rr);
362 4 : TALER_EXCHANGE_free_coin_conflict_ (&rr.hr,
363 : &rr.details.conflict);
364 4 : TALER_EXCHANGE_post_recoup_withdraw_cancel (prwh);
365 : }
366 :
367 :
368 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *
369 22 : TALER_EXCHANGE_post_recoup_withdraw_create (
370 : struct GNUNET_CURL_Context *ctx,
371 : const char *url,
372 : struct TALER_EXCHANGE_Keys *keys,
373 : const struct TALER_ReservePublicKeyP *reserve_pub,
374 : const struct TALER_HashBlindedPlanchetsP *planchets_h,
375 : const struct TALER_BlindingMasterSeedP *blinding_seed,
376 : size_t num_coins,
377 : const struct TALER_EXCHANGE_RecoupCoin coins[static num_coins])
378 22 : {
379 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh;
380 : json_t *coin_data;
381 :
382 22 : prwh = GNUNET_new (struct TALER_EXCHANGE_PostRecoupWithdrawHandle);
383 22 : prwh->recouped_pubs = GNUNET_new_array (num_coins,
384 : struct TALER_CoinSpendPublicKeyP);
385 22 : prwh->recouped_infos = GNUNET_new_array (
386 : num_coins,
387 : struct TALER_EXCHANGE_RecoupedCoinInfo_);
388 22 : coin_data = TALER_EXCHANGE_recoup_coin_data_ (num_coins,
389 : coins,
390 : blinding_seed,
391 : false, /* not for melt */
392 : prwh->recouped_pubs,
393 : prwh->recouped_infos,
394 : &prwh->num_recouped);
395 22 : if (NULL == coin_data)
396 : {
397 0 : GNUNET_break (0);
398 0 : GNUNET_free (prwh->recouped_pubs);
399 0 : GNUNET_free (prwh->recouped_infos);
400 0 : GNUNET_free (prwh);
401 0 : return NULL;
402 : }
403 22 : prwh->ctx = ctx;
404 22 : prwh->base_url = GNUNET_strdup (url);
405 22 : prwh->keys = TALER_EXCHANGE_keys_incref (keys);
406 22 : prwh->reserve_pub = *reserve_pub;
407 22 : prwh->planchets_h = *planchets_h;
408 22 : prwh->body = GNUNET_JSON_PACK (
409 : GNUNET_JSON_pack_data_auto ("reserve_pub",
410 : reserve_pub),
411 : GNUNET_JSON_pack_data_auto ("planchets_h",
412 : planchets_h),
413 : GNUNET_JSON_pack_array_steal ("coin_data",
414 : coin_data));
415 22 : return prwh;
416 : }
417 :
418 :
419 : enum TALER_ErrorCode
420 22 : TALER_EXCHANGE_post_recoup_withdraw_start (
421 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh,
422 : TALER_EXCHANGE_PostRecoupWithdrawCallback cb,
423 : TALER_EXCHANGE_POST_RECOUP_WITHDRAW_RESULT_CLOSURE *cb_cls)
424 : {
425 : CURL *eh;
426 :
427 22 : prwh->cb = cb;
428 22 : prwh->cb_cls = cb_cls;
429 22 : prwh->url = TALER_url_join (prwh->base_url,
430 : "recoup-withdraw",
431 : NULL);
432 22 : if (NULL == prwh->url)
433 : {
434 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
435 : "Could not construct request URL.\n");
436 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
437 : }
438 22 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
439 : "URL for recoup-withdraw: `%s'\n",
440 : prwh->url);
441 22 : eh = TALER_EXCHANGE_curl_easy_get_ (prwh->url);
442 44 : if ( (NULL == eh) ||
443 : (GNUNET_OK !=
444 22 : TALER_curl_easy_post (&prwh->post_ctx,
445 : eh,
446 22 : prwh->body)) )
447 : {
448 0 : GNUNET_break (0);
449 0 : if (NULL != eh)
450 0 : curl_easy_cleanup (eh);
451 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
452 : }
453 44 : prwh->job = GNUNET_CURL_job_add2 (prwh->ctx,
454 : eh,
455 22 : prwh->post_ctx.headers,
456 : &handle_recoup_withdraw_finished,
457 : prwh);
458 22 : if (NULL == prwh->job)
459 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
460 22 : return TALER_EC_NONE;
461 : }
462 :
463 :
464 : void
465 22 : TALER_EXCHANGE_post_recoup_withdraw_cancel (
466 : struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh)
467 : {
468 22 : if (NULL != prwh->job)
469 : {
470 0 : GNUNET_CURL_job_cancel (prwh->job);
471 0 : prwh->job = NULL;
472 : }
473 22 : TALER_curl_easy_post_finished (&prwh->post_ctx);
474 22 : GNUNET_free (prwh->url);
475 22 : GNUNET_free (prwh->base_url);
476 22 : GNUNET_free (prwh->recouped_pubs);
477 22 : GNUNET_free (prwh->recouped_infos);
478 22 : json_decref (prwh->body);
479 22 : TALER_EXCHANGE_keys_decref (prwh->keys);
480 22 : GNUNET_free (prwh);
481 22 : }
482 :
483 :
484 : /* end of exchange_api_post-recoup-withdraw.c */
|