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-reserves-RESERVE_PUB-open.c
19 : * @brief Implementation of the POST /reserves/$RESERVE_PUB/open requests
20 : * @author Christian Grothoff
21 : */
22 : #include "taler/platform.h"
23 : #include <jansson.h>
24 : #include <microhttpd.h> /* just for HTTP open 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_exchange_service.h"
29 : #include "taler/taler_json_lib.h"
30 : #include "exchange_api_common.h"
31 : #include "exchange_api_handle.h"
32 : #include "taler/taler_signatures.h"
33 : #include "exchange_api_curl_defaults.h"
34 :
35 :
36 : /**
37 : * Information we keep per coin to validate the reply.
38 : */
39 : struct CoinData
40 : {
41 : /**
42 : * Public key of the coin.
43 : */
44 : struct TALER_CoinSpendPublicKeyP coin_pub;
45 :
46 : /**
47 : * Signature by the coin.
48 : */
49 : struct TALER_CoinSpendSignatureP coin_sig;
50 :
51 : /**
52 : * The hash of the denomination's public key
53 : */
54 : struct TALER_DenominationHashP h_denom_pub;
55 :
56 : /**
57 : * How much did this coin contribute.
58 : */
59 : struct TALER_Amount contribution;
60 : };
61 :
62 :
63 : /**
64 : * @brief A POST /reserves/$RID/open Handle
65 : */
66 : struct TALER_EXCHANGE_PostReservesOpenHandle
67 : {
68 :
69 : /**
70 : * Reference to the execution context.
71 : */
72 : struct GNUNET_CURL_Context *ctx;
73 :
74 : /**
75 : * Base URL of the exchange.
76 : */
77 : char *base_url;
78 :
79 : /**
80 : * The url for this request, set during _start.
81 : */
82 : char *url;
83 :
84 : /**
85 : * Context for #TEH_curl_easy_post(). Keeps the data that must
86 : * persist for Curl to make the upload.
87 : */
88 : struct TALER_CURL_PostContext post_ctx;
89 :
90 : /**
91 : * Handle for the request.
92 : */
93 : struct GNUNET_CURL_Job *job;
94 :
95 : /**
96 : * Function to call with the result.
97 : */
98 : TALER_EXCHANGE_PostReservesOpenCallback cb;
99 :
100 : /**
101 : * Closure for @a cb.
102 : */
103 : TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls;
104 :
105 : /**
106 : * The keys of the exchange this request handle will use
107 : */
108 : struct TALER_EXCHANGE_Keys *keys;
109 :
110 : /**
111 : * Public key of the reserve we are querying.
112 : */
113 : struct TALER_ReservePublicKeyP reserve_pub;
114 :
115 : /**
116 : * Information we keep per coin to validate the reply.
117 : */
118 : struct CoinData *coins;
119 :
120 : /**
121 : * Length of the @e coins array.
122 : */
123 : unsigned int num_coins;
124 :
125 : /**
126 : * Pre-built JSON body for the request.
127 : */
128 : json_t *body;
129 :
130 : };
131 :
132 :
133 : /**
134 : * We received an #MHD_HTTP_OK open code. Handle the JSON
135 : * response.
136 : *
137 : * @param proh handle of the request
138 : * @param j JSON response
139 : * @return #GNUNET_OK on success
140 : */
141 : static enum GNUNET_GenericReturnValue
142 4 : handle_reserves_open_ok (struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
143 : const json_t *j)
144 : {
145 4 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
146 : .hr.reply = j,
147 : .hr.http_status = MHD_HTTP_OK,
148 : };
149 : struct GNUNET_JSON_Specification spec[] = {
150 4 : TALER_JSON_spec_amount_any ("open_cost",
151 : &rs.details.ok.open_cost),
152 4 : GNUNET_JSON_spec_timestamp ("reserve_expiration",
153 : &rs.details.ok.expiration_time),
154 4 : GNUNET_JSON_spec_end ()
155 : };
156 :
157 4 : if (GNUNET_OK !=
158 4 : GNUNET_JSON_parse (j,
159 : spec,
160 : NULL,
161 : NULL))
162 : {
163 0 : GNUNET_break_op (0);
164 0 : return GNUNET_SYSERR;
165 : }
166 4 : proh->cb (proh->cb_cls,
167 : &rs);
168 4 : proh->cb = NULL;
169 4 : GNUNET_JSON_parse_free (spec);
170 4 : return GNUNET_OK;
171 : }
172 :
173 :
174 : /**
175 : * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON
176 : * response.
177 : *
178 : * @param proh handle of the request
179 : * @param j JSON response
180 : * @return #GNUNET_OK on success
181 : */
182 : static enum GNUNET_GenericReturnValue
183 2 : handle_reserves_open_pr (struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
184 : const json_t *j)
185 : {
186 2 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
187 : .hr.reply = j,
188 : .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED,
189 : };
190 : struct GNUNET_JSON_Specification spec[] = {
191 2 : TALER_JSON_spec_amount_any ("open_cost",
192 : &rs.details.payment_required.open_cost),
193 2 : GNUNET_JSON_spec_timestamp ("reserve_expiration",
194 : &rs.details.payment_required.expiration_time),
195 2 : GNUNET_JSON_spec_end ()
196 : };
197 :
198 2 : if (GNUNET_OK !=
199 2 : GNUNET_JSON_parse (j,
200 : spec,
201 : NULL,
202 : NULL))
203 : {
204 0 : GNUNET_break_op (0);
205 0 : return GNUNET_SYSERR;
206 : }
207 2 : proh->cb (proh->cb_cls,
208 : &rs);
209 2 : proh->cb = NULL;
210 2 : GNUNET_JSON_parse_free (spec);
211 2 : return GNUNET_OK;
212 : }
213 :
214 :
215 : /**
216 : * We received an #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS open code. Handle the JSON
217 : * response.
218 : *
219 : * @param proh handle of the request
220 : * @param j JSON response
221 : * @return #GNUNET_OK on success
222 : */
223 : static enum GNUNET_GenericReturnValue
224 0 : handle_reserves_open_kyc (struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
225 : const json_t *j)
226 : {
227 0 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
228 : .hr.reply = j,
229 : .hr.http_status = MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
230 : };
231 : struct GNUNET_JSON_Specification spec[] = {
232 0 : GNUNET_JSON_spec_fixed_auto (
233 : "h_payto",
234 : &rs.details.unavailable_for_legal_reasons.h_payto),
235 0 : GNUNET_JSON_spec_uint64 (
236 : "requirement_row",
237 : &rs.details.unavailable_for_legal_reasons.requirement_row),
238 0 : GNUNET_JSON_spec_end ()
239 : };
240 :
241 0 : if (GNUNET_OK !=
242 0 : GNUNET_JSON_parse (j,
243 : spec,
244 : NULL,
245 : NULL))
246 : {
247 0 : GNUNET_break_op (0);
248 0 : return GNUNET_SYSERR;
249 : }
250 0 : proh->cb (proh->cb_cls,
251 : &rs);
252 0 : proh->cb = NULL;
253 0 : GNUNET_JSON_parse_free (spec);
254 0 : return GNUNET_OK;
255 : }
256 :
257 :
258 : /**
259 : * Function called when we're done processing the
260 : * HTTP /reserves/$RID/open request.
261 : *
262 : * @param cls the `struct TALER_EXCHANGE_PostReservesOpenHandle`
263 : * @param response_code HTTP response code, 0 on error
264 : * @param response parsed JSON result, NULL on error
265 : */
266 : static void
267 6 : handle_reserves_open_finished (void *cls,
268 : long response_code,
269 : const void *response)
270 : {
271 6 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls;
272 6 : const json_t *j = response;
273 6 : struct TALER_EXCHANGE_PostReservesOpenResponse rs = {
274 : .hr.reply = j,
275 6 : .hr.http_status = (unsigned int) response_code
276 : };
277 :
278 6 : proh->job = NULL;
279 6 : switch (response_code)
280 : {
281 0 : case 0:
282 0 : rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
283 0 : break;
284 4 : case MHD_HTTP_OK:
285 4 : if (GNUNET_OK !=
286 4 : handle_reserves_open_ok (proh,
287 : j))
288 : {
289 0 : GNUNET_break_op (0);
290 0 : rs.hr.http_status = 0;
291 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
292 : }
293 4 : break;
294 0 : case MHD_HTTP_BAD_REQUEST:
295 : /* This should never happen, either us or the exchange is buggy
296 : (or API version conflict); just pass JSON reply to the application */
297 0 : GNUNET_break (0);
298 0 : json_dumpf (j,
299 : stderr,
300 : JSON_INDENT (2));
301 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
302 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
303 0 : break;
304 2 : case MHD_HTTP_PAYMENT_REQUIRED:
305 2 : if (GNUNET_OK !=
306 2 : handle_reserves_open_pr (proh,
307 : j))
308 : {
309 0 : GNUNET_break_op (0);
310 0 : rs.hr.http_status = 0;
311 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
312 : }
313 2 : break;
314 0 : case MHD_HTTP_FORBIDDEN:
315 : /* This should never happen, either us or the exchange is buggy
316 : (or API version conflict); just pass JSON reply to the application */
317 0 : GNUNET_break (0);
318 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
319 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
320 0 : break;
321 0 : case MHD_HTTP_NOT_FOUND:
322 : /* Nothing really to verify, this should never
323 : happen, we should pass the JSON reply to the application */
324 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
325 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
326 0 : break;
327 0 : case MHD_HTTP_CONFLICT:
328 : {
329 0 : const struct CoinData *cd = NULL;
330 : struct GNUNET_JSON_Specification spec[] = {
331 0 : GNUNET_JSON_spec_fixed_auto ("coin_pub",
332 : &rs.details.conflict.coin_pub),
333 0 : GNUNET_JSON_spec_end ()
334 : };
335 :
336 0 : if (GNUNET_OK !=
337 0 : GNUNET_JSON_parse (j,
338 : spec,
339 : NULL,
340 : NULL))
341 : {
342 0 : GNUNET_break_op (0);
343 0 : rs.hr.http_status = 0;
344 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
345 0 : break;
346 : }
347 0 : for (unsigned int i = 0; i < proh->num_coins; i++)
348 : {
349 0 : const struct CoinData *cdi = &proh->coins[i];
350 :
351 0 : if (0 == GNUNET_memcmp (&rs.details.conflict.coin_pub,
352 : &cdi->coin_pub))
353 : {
354 0 : cd = cdi;
355 0 : break;
356 : }
357 : }
358 0 : if (NULL == cd)
359 : {
360 0 : GNUNET_break_op (0);
361 0 : rs.hr.http_status = 0;
362 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
363 0 : break;
364 : }
365 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
366 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
367 0 : break;
368 : }
369 0 : case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
370 0 : if (GNUNET_OK !=
371 0 : handle_reserves_open_kyc (proh,
372 : j))
373 : {
374 0 : GNUNET_break_op (0);
375 0 : rs.hr.http_status = 0;
376 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
377 : }
378 0 : break;
379 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
380 : /* Server had an internal issue; we should retry, but this API
381 : leaves this to the application */
382 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
383 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
384 0 : break;
385 0 : default:
386 : /* unexpected response code */
387 0 : GNUNET_break_op (0);
388 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
389 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
390 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
391 : "Unexpected response code %u/%d for reserves open\n",
392 : (unsigned int) response_code,
393 : (int) rs.hr.ec);
394 0 : break;
395 : }
396 6 : if (NULL != proh->cb)
397 : {
398 0 : proh->cb (proh->cb_cls,
399 : &rs);
400 0 : proh->cb = NULL;
401 : }
402 6 : TALER_EXCHANGE_post_reserves_open_cancel (proh);
403 6 : }
404 :
405 :
406 : struct TALER_EXCHANGE_PostReservesOpenHandle *
407 6 : TALER_EXCHANGE_post_reserves_open_create (
408 : struct GNUNET_CURL_Context *ctx,
409 : const char *url,
410 : struct TALER_EXCHANGE_Keys *keys,
411 : const struct TALER_ReservePrivateKeyP *reserve_priv,
412 : const struct TALER_Amount *reserve_contribution,
413 : unsigned int coin_payments_length,
414 : const struct TALER_EXCHANGE_PurseDeposit coin_payments[
415 : static coin_payments_length],
416 : struct GNUNET_TIME_Timestamp expiration_time,
417 : uint32_t min_purses)
418 6 : {
419 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh;
420 : struct GNUNET_TIME_Timestamp ts;
421 : struct TALER_ReserveSignatureP reserve_sig;
422 : json_t *cpa;
423 :
424 6 : proh = GNUNET_new (struct TALER_EXCHANGE_PostReservesOpenHandle);
425 6 : proh->ctx = ctx;
426 6 : proh->base_url = GNUNET_strdup (url);
427 6 : proh->keys = TALER_EXCHANGE_keys_incref (keys);
428 6 : ts = GNUNET_TIME_timestamp_get ();
429 6 : GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
430 : &proh->reserve_pub.eddsa_pub);
431 6 : TALER_wallet_reserve_open_sign (reserve_contribution,
432 : ts,
433 : expiration_time,
434 : min_purses,
435 : reserve_priv,
436 : &reserve_sig);
437 6 : proh->coins = GNUNET_new_array (coin_payments_length,
438 : struct CoinData);
439 6 : proh->num_coins = coin_payments_length;
440 6 : cpa = json_array ();
441 6 : GNUNET_assert (NULL != cpa);
442 8 : for (unsigned int i = 0; i < coin_payments_length; i++)
443 : {
444 2 : const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i];
445 2 : const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof;
446 : struct TALER_AgeCommitmentHashP ahac;
447 2 : struct TALER_AgeCommitmentHashP *achp = NULL;
448 2 : struct CoinData *cd = &proh->coins[i];
449 : json_t *cp;
450 :
451 2 : cd->contribution = pd->amount;
452 2 : cd->h_denom_pub = pd->h_denom_pub;
453 2 : if (NULL != acp)
454 : {
455 0 : TALER_age_commitment_hash (&acp->commitment,
456 : &ahac);
457 0 : achp = &ahac;
458 : }
459 2 : TALER_wallet_reserve_open_deposit_sign (&pd->amount,
460 : &reserve_sig,
461 : &pd->coin_priv,
462 : &cd->coin_sig);
463 2 : GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv,
464 : &cd->coin_pub.eddsa_pub);
465 :
466 2 : cp = GNUNET_JSON_PACK (
467 : GNUNET_JSON_pack_allow_null (
468 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
469 : achp)),
470 : TALER_JSON_pack_amount ("amount",
471 : &pd->amount),
472 : GNUNET_JSON_pack_data_auto ("denom_pub_hash",
473 : &pd->h_denom_pub),
474 : TALER_JSON_pack_denom_sig ("ub_sig",
475 : &pd->denom_sig),
476 : GNUNET_JSON_pack_data_auto ("coin_pub",
477 : &cd->coin_pub),
478 : GNUNET_JSON_pack_data_auto ("coin_sig",
479 : &cd->coin_sig));
480 2 : GNUNET_assert (0 ==
481 : json_array_append_new (cpa,
482 : cp));
483 : }
484 6 : proh->body = GNUNET_JSON_PACK (
485 : GNUNET_JSON_pack_timestamp ("request_timestamp",
486 : ts),
487 : GNUNET_JSON_pack_timestamp ("reserve_expiration",
488 : expiration_time),
489 : GNUNET_JSON_pack_array_steal ("payments",
490 : cpa),
491 : TALER_JSON_pack_amount ("reserve_payment",
492 : reserve_contribution),
493 : GNUNET_JSON_pack_uint64 ("purse_limit",
494 : min_purses),
495 : GNUNET_JSON_pack_data_auto ("reserve_sig",
496 : &reserve_sig));
497 6 : if (NULL == proh->body)
498 : {
499 0 : GNUNET_break (0);
500 0 : GNUNET_free (proh->coins);
501 0 : GNUNET_free (proh->base_url);
502 0 : TALER_EXCHANGE_keys_decref (proh->keys);
503 0 : GNUNET_free (proh);
504 0 : return NULL;
505 : }
506 6 : return proh;
507 : }
508 :
509 :
510 : enum TALER_ErrorCode
511 6 : TALER_EXCHANGE_post_reserves_open_start (
512 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh,
513 : TALER_EXCHANGE_PostReservesOpenCallback cb,
514 : TALER_EXCHANGE_POST_RESERVES_OPEN_RESULT_CLOSURE *cb_cls)
515 : {
516 : CURL *eh;
517 : char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
518 :
519 6 : proh->cb = cb;
520 6 : proh->cb_cls = cb_cls;
521 : {
522 : char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
523 : char *end;
524 :
525 6 : end = GNUNET_STRINGS_data_to_string (
526 6 : &proh->reserve_pub,
527 : sizeof (proh->reserve_pub),
528 : pub_str,
529 : sizeof (pub_str));
530 6 : *end = '\0';
531 6 : GNUNET_snprintf (arg_str,
532 : sizeof (arg_str),
533 : "reserves/%s/open",
534 : pub_str);
535 : }
536 6 : proh->url = TALER_url_join (proh->base_url,
537 : arg_str,
538 : NULL);
539 6 : if (NULL == proh->url)
540 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
541 6 : eh = TALER_EXCHANGE_curl_easy_get_ (proh->url);
542 12 : if ( (NULL == eh) ||
543 : (GNUNET_OK !=
544 6 : TALER_curl_easy_post (&proh->post_ctx,
545 : eh,
546 6 : proh->body)) )
547 : {
548 0 : GNUNET_break (0);
549 0 : if (NULL != eh)
550 0 : curl_easy_cleanup (eh);
551 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
552 : }
553 12 : proh->job = GNUNET_CURL_job_add2 (proh->ctx,
554 : eh,
555 6 : proh->post_ctx.headers,
556 : &handle_reserves_open_finished,
557 : proh);
558 6 : if (NULL == proh->job)
559 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
560 6 : return TALER_EC_NONE;
561 : }
562 :
563 :
564 : void
565 6 : TALER_EXCHANGE_post_reserves_open_cancel (
566 : struct TALER_EXCHANGE_PostReservesOpenHandle *proh)
567 : {
568 6 : if (NULL != proh->job)
569 : {
570 0 : GNUNET_CURL_job_cancel (proh->job);
571 0 : proh->job = NULL;
572 : }
573 6 : TALER_curl_easy_post_finished (&proh->post_ctx);
574 6 : json_decref (proh->body);
575 6 : GNUNET_free (proh->coins);
576 6 : GNUNET_free (proh->url);
577 6 : GNUNET_free (proh->base_url);
578 6 : TALER_EXCHANGE_keys_decref (proh->keys);
579 6 : GNUNET_free (proh);
580 6 : }
581 :
582 :
583 : /* end of exchange_api_post-reserves-RESERVE_PUB-open.c */
|