Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2022 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 taler-exchange-httpd_purses_deposit.c
18 : * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and
19 : * verifies the coin signature before handing things off
20 : * to the database.
21 : * @author Christian Grothoff
22 : */
23 : #include "taler/platform.h"
24 : #include <gnunet/gnunet_util_lib.h>
25 : #include <gnunet/gnunet_json_lib.h>
26 : #include <jansson.h>
27 : #include <microhttpd.h>
28 : #include "taler/taler_dbevents.h"
29 : #include "taler/taler_json_lib.h"
30 : #include "taler/taler_mhd_lib.h"
31 : #include "taler-exchange-httpd_common_deposit.h"
32 : #include "taler-exchange-httpd_purses_deposit.h"
33 : #include "taler-exchange-httpd_responses.h"
34 : #include "taler/taler_exchangedb_lib.h"
35 : #include "taler-exchange-httpd_keys.h"
36 :
37 :
38 : /**
39 : * Closure for #deposit_transaction.
40 : */
41 : struct PurseDepositContext
42 : {
43 : /**
44 : * Public key of the purse we are creating.
45 : */
46 : const struct TALER_PurseContractPublicKeyP *purse_pub;
47 :
48 : /**
49 : * Total amount to be put into the purse.
50 : */
51 : struct TALER_Amount amount;
52 :
53 : /**
54 : * Total actually deposited by all the coins.
55 : */
56 : struct TALER_Amount deposit_total;
57 :
58 : /**
59 : * When should the purse expire.
60 : */
61 : struct GNUNET_TIME_Timestamp purse_expiration;
62 :
63 : /**
64 : * Hash of the contract (needed for signing).
65 : */
66 : struct TALER_PrivateContractHashP h_contract_terms;
67 :
68 : /**
69 : * Our current time.
70 : */
71 : struct GNUNET_TIME_Timestamp exchange_timestamp;
72 :
73 : /**
74 : * Array of coins being deposited.
75 : */
76 : struct TEH_PurseDepositedCoin *coins;
77 :
78 : /**
79 : * Length of the @e coins array.
80 : */
81 : unsigned int num_coins;
82 :
83 : /**
84 : * Minimum age for deposits into this purse.
85 : */
86 : uint32_t min_age;
87 : };
88 :
89 :
90 : /**
91 : * Send confirmation of purse creation success to client.
92 : *
93 : * @param connection connection to the client
94 : * @param pcc details about the request that succeeded
95 : * @return MHD result code
96 : */
97 : static MHD_RESULT
98 5 : reply_deposit_success (struct MHD_Connection *connection,
99 : const struct PurseDepositContext *pcc)
100 : {
101 : struct TALER_ExchangePublicKeyP pub;
102 : struct TALER_ExchangeSignatureP sig;
103 : enum TALER_ErrorCode ec;
104 :
105 5 : if (TALER_EC_NONE !=
106 5 : (ec = TALER_exchange_online_purse_created_sign (
107 : &TEH_keys_exchange_sign_,
108 : pcc->exchange_timestamp,
109 : pcc->purse_expiration,
110 : &pcc->amount,
111 : &pcc->deposit_total,
112 5 : pcc->purse_pub,
113 : &pcc->h_contract_terms,
114 : &pub,
115 : &sig)))
116 : {
117 0 : GNUNET_break (0);
118 0 : return TALER_MHD_reply_with_ec (connection,
119 : ec,
120 : NULL);
121 : }
122 5 : return TALER_MHD_REPLY_JSON_PACK (
123 : connection,
124 : MHD_HTTP_OK,
125 : TALER_JSON_pack_amount ("total_deposited",
126 : &pcc->deposit_total),
127 : TALER_JSON_pack_amount ("purse_value_after_fees",
128 : &pcc->amount),
129 : GNUNET_JSON_pack_timestamp ("exchange_timestamp",
130 : pcc->exchange_timestamp),
131 : GNUNET_JSON_pack_timestamp ("purse_expiration",
132 : pcc->purse_expiration),
133 : GNUNET_JSON_pack_data_auto ("h_contract_terms",
134 : &pcc->h_contract_terms),
135 : GNUNET_JSON_pack_data_auto ("exchange_sig",
136 : &sig),
137 : GNUNET_JSON_pack_data_auto ("exchange_pub",
138 : &pub));
139 : }
140 :
141 :
142 : /**
143 : * Execute database transaction for /purses/$PID/deposit. Runs the transaction
144 : * logic; IF it returns a non-error code, the transaction logic MUST NOT queue
145 : * a MHD response. IF it returns an hard error, the transaction logic MUST
146 : * queue a MHD response and set @a mhd_ret. IF it returns the soft error
147 : * code, the function MAY be called again to retry and MUST not queue a MHD
148 : * response.
149 : *
150 : * @param cls a `struct PurseDepositContext`
151 : * @param connection MHD request context
152 : * @param[out] mhd_ret set to MHD status on error
153 : * @return transaction status
154 : */
155 : static enum GNUNET_DB_QueryStatus
156 7 : deposit_transaction (void *cls,
157 : struct MHD_Connection *connection,
158 : MHD_RESULT *mhd_ret)
159 : {
160 7 : struct PurseDepositContext *pcc = cls;
161 : enum GNUNET_DB_QueryStatus qs;
162 :
163 7 : qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
164 12 : for (unsigned int i = 0; i<pcc->num_coins; i++)
165 : {
166 7 : struct TEH_PurseDepositedCoin *coin = &pcc->coins[i];
167 7 : bool balance_ok = false;
168 7 : bool conflict = true;
169 7 : bool too_late = true;
170 :
171 7 : qs = TEH_make_coin_known (&coin->cpi,
172 : connection,
173 : &coin->known_coin_id,
174 : mhd_ret);
175 7 : if (qs < 0)
176 2 : return qs;
177 7 : qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls,
178 : pcc->purse_pub,
179 7 : &coin->cpi.coin_pub,
180 7 : &coin->amount,
181 7 : &coin->coin_sig,
182 7 : &coin->amount_minus_fee,
183 : &balance_ok,
184 : &too_late,
185 : &conflict);
186 7 : if (qs <= 0)
187 : {
188 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
189 0 : return qs;
190 0 : GNUNET_break (0 != qs);
191 0 : TALER_LOG_WARNING (
192 : "Failed to store purse deposit information in database\n");
193 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
194 : MHD_HTTP_INTERNAL_SERVER_ERROR,
195 : TALER_EC_GENERIC_DB_STORE_FAILED,
196 : "do purse deposit");
197 0 : return GNUNET_DB_STATUS_HARD_ERROR;
198 : }
199 7 : if (! balance_ok)
200 : {
201 : *mhd_ret
202 4 : = TEH_RESPONSE_reply_coin_insufficient_funds (
203 : connection,
204 : TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
205 2 : &coin->cpi.denom_pub_hash,
206 2 : &coin->cpi.coin_pub);
207 2 : return GNUNET_DB_STATUS_HARD_ERROR;
208 : }
209 5 : if (too_late)
210 : {
211 0 : TEH_plugin->rollback (TEH_plugin->cls);
212 : *mhd_ret
213 0 : = TALER_MHD_reply_with_ec (
214 : connection,
215 : TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY,
216 : NULL);
217 0 : return GNUNET_DB_STATUS_HARD_ERROR;
218 : }
219 5 : if (conflict)
220 : {
221 : struct TALER_Amount amount;
222 : struct TALER_CoinSpendPublicKeyP coin_pub;
223 : struct TALER_CoinSpendSignatureP coin_sig;
224 : struct TALER_DenominationHashP h_denom_pub;
225 : struct TALER_AgeCommitmentHashP phac;
226 0 : char *partner_url = NULL;
227 :
228 0 : TEH_plugin->rollback (TEH_plugin->cls);
229 0 : qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls,
230 : pcc->purse_pub,
231 0 : &coin->cpi.coin_pub,
232 : &amount,
233 : &h_denom_pub,
234 : &phac,
235 : &coin_sig,
236 : &partner_url);
237 0 : if (qs < 0)
238 : {
239 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
240 0 : TALER_LOG_WARNING (
241 : "Failed to fetch purse deposit information from database\n");
242 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
243 : MHD_HTTP_INTERNAL_SERVER_ERROR,
244 : TALER_EC_GENERIC_DB_FETCH_FAILED,
245 : "get purse deposit");
246 0 : return GNUNET_DB_STATUS_HARD_ERROR;
247 : }
248 :
249 : *mhd_ret
250 0 : = TALER_MHD_REPLY_JSON_PACK (
251 : connection,
252 : MHD_HTTP_CONFLICT,
253 : TALER_JSON_pack_ec (
254 : TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA),
255 : GNUNET_JSON_pack_data_auto ("coin_pub",
256 : &coin_pub),
257 : GNUNET_JSON_pack_data_auto ("h_denom_pub",
258 : &h_denom_pub),
259 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
260 : &phac),
261 : GNUNET_JSON_pack_data_auto ("coin_sig",
262 : &coin_sig),
263 : GNUNET_JSON_pack_allow_null (
264 : GNUNET_JSON_pack_string ("partner_url",
265 : partner_url)),
266 : TALER_JSON_pack_amount ("amount",
267 : &amount));
268 0 : GNUNET_free (partner_url);
269 0 : return GNUNET_DB_STATUS_HARD_ERROR;
270 : }
271 : }
272 5 : return qs;
273 : }
274 :
275 :
276 : /**
277 : * Parse a coin and check signature of the coin and the denomination
278 : * signature over the coin.
279 : *
280 : * @param[in,out] connection our HTTP connection
281 : * @param[in,out] pcc request context
282 : * @param[out] coin coin to initialize
283 : * @param jcoin coin to parse
284 : * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned,
285 : * #GNUNET_SYSERR on failure and no error could be returned
286 : */
287 : static enum GNUNET_GenericReturnValue
288 7 : parse_coin (struct MHD_Connection *connection,
289 : struct PurseDepositContext *pcc,
290 : struct TEH_PurseDepositedCoin *coin,
291 : const json_t *jcoin)
292 : {
293 : enum GNUNET_GenericReturnValue iret;
294 :
295 7 : if (GNUNET_OK !=
296 7 : (iret = TEH_common_purse_deposit_parse_coin (connection,
297 : coin,
298 : jcoin)))
299 0 : return iret;
300 7 : if (GNUNET_OK !=
301 7 : (iret = TEH_common_deposit_check_purse_deposit (
302 : connection,
303 : coin,
304 : pcc->purse_pub,
305 : pcc->min_age)))
306 0 : return iret;
307 7 : if (0 >
308 7 : TALER_amount_add (&pcc->deposit_total,
309 7 : &pcc->deposit_total,
310 7 : &coin->amount_minus_fee))
311 : {
312 0 : GNUNET_break (0);
313 0 : return TALER_MHD_reply_with_error (connection,
314 : MHD_HTTP_INTERNAL_SERVER_ERROR,
315 : TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
316 : "total deposit contribution");
317 : }
318 7 : return GNUNET_OK;
319 : }
320 :
321 :
322 : MHD_RESULT
323 7 : TEH_handler_purses_deposit (
324 : struct TEH_RequestContext *rc,
325 : const struct TALER_PurseContractPublicKeyP *purse_pub,
326 : const json_t *root)
327 : {
328 7 : struct MHD_Connection *connection = rc->connection;
329 7 : struct PurseDepositContext pcc = {
330 : .purse_pub = purse_pub,
331 7 : .exchange_timestamp = GNUNET_TIME_timestamp_get ()
332 : };
333 : const json_t *deposits;
334 : json_t *deposit;
335 : unsigned int idx;
336 : struct GNUNET_JSON_Specification spec[] = {
337 7 : GNUNET_JSON_spec_array_const ("deposits",
338 : &deposits),
339 7 : GNUNET_JSON_spec_end ()
340 : };
341 :
342 : {
343 : enum GNUNET_GenericReturnValue res;
344 :
345 7 : res = TALER_MHD_parse_json_data (connection,
346 : root,
347 : spec);
348 7 : if (GNUNET_SYSERR == res)
349 : {
350 0 : GNUNET_break (0);
351 0 : return MHD_NO; /* hard failure */
352 : }
353 7 : if (GNUNET_NO == res)
354 : {
355 0 : GNUNET_break_op (0);
356 0 : return MHD_YES; /* failure */
357 : }
358 : }
359 7 : GNUNET_assert (GNUNET_OK ==
360 : TALER_amount_set_zero (TEH_currency,
361 : &pcc.deposit_total));
362 7 : pcc.num_coins = (unsigned int) json_array_size (deposits);
363 14 : if ( (0 == pcc.num_coins) ||
364 7 : (((size_t) pcc.num_coins) != json_array_size (deposits)) ||
365 7 : (pcc.num_coins > TALER_MAX_COINS) )
366 : {
367 0 : GNUNET_break_op (0);
368 0 : return TALER_MHD_reply_with_error (connection,
369 : MHD_HTTP_BAD_REQUEST,
370 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
371 : "deposits");
372 : }
373 :
374 : {
375 : enum GNUNET_DB_QueryStatus qs;
376 : struct GNUNET_TIME_Timestamp create_timestamp;
377 : struct GNUNET_TIME_Timestamp merge_timestamp;
378 : bool was_deleted;
379 : bool was_refunded;
380 :
381 7 : qs = TEH_plugin->select_purse (
382 7 : TEH_plugin->cls,
383 : pcc.purse_pub,
384 : &create_timestamp,
385 : &pcc.purse_expiration,
386 : &pcc.amount,
387 : &pcc.deposit_total,
388 : &pcc.h_contract_terms,
389 : &merge_timestamp,
390 : &was_deleted,
391 : &was_refunded);
392 7 : switch (qs)
393 : {
394 0 : case GNUNET_DB_STATUS_HARD_ERROR:
395 0 : GNUNET_break (0);
396 0 : return TALER_MHD_reply_with_error (connection,
397 : MHD_HTTP_INTERNAL_SERVER_ERROR,
398 : TALER_EC_GENERIC_DB_FETCH_FAILED,
399 : "select purse");
400 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
401 0 : GNUNET_break (0);
402 0 : return TALER_MHD_reply_with_error (connection,
403 : MHD_HTTP_INTERNAL_SERVER_ERROR,
404 : TALER_EC_GENERIC_DB_FETCH_FAILED,
405 : "select purse");
406 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
407 0 : return TALER_MHD_reply_with_error (connection,
408 : MHD_HTTP_NOT_FOUND,
409 : TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN,
410 : NULL);
411 7 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
412 7 : break; /* handled below */
413 : }
414 7 : if (was_refunded ||
415 : was_deleted)
416 : {
417 0 : return TALER_MHD_reply_with_error (
418 : connection,
419 : MHD_HTTP_GONE,
420 : was_deleted
421 0 : ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED
422 : : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED,
423 : GNUNET_TIME_timestamp2s (pcc.purse_expiration));
424 : }
425 : }
426 :
427 : /* parse deposits */
428 7 : pcc.coins = GNUNET_new_array (pcc.num_coins,
429 : struct TEH_PurseDepositedCoin);
430 14 : json_array_foreach (deposits, idx, deposit)
431 : {
432 : enum GNUNET_GenericReturnValue res;
433 7 : struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx];
434 :
435 7 : res = parse_coin (connection,
436 : &pcc,
437 : coin,
438 : deposit);
439 7 : if (GNUNET_OK != res)
440 : {
441 0 : for (unsigned int i = 0; i<idx; i++)
442 0 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
443 0 : GNUNET_free (pcc.coins);
444 0 : return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
445 : }
446 : }
447 :
448 7 : if (GNUNET_SYSERR ==
449 7 : TEH_plugin->preflight (TEH_plugin->cls))
450 : {
451 0 : GNUNET_break (0);
452 0 : for (unsigned int i = 0; i<pcc.num_coins; i++)
453 0 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
454 0 : GNUNET_free (pcc.coins);
455 0 : return TALER_MHD_reply_with_error (connection,
456 : MHD_HTTP_INTERNAL_SERVER_ERROR,
457 : TALER_EC_GENERIC_DB_START_FAILED,
458 : "preflight failure");
459 : }
460 :
461 : /* execute transaction */
462 : {
463 : MHD_RESULT mhd_ret;
464 :
465 7 : if (GNUNET_OK !=
466 7 : TEH_DB_run_transaction (connection,
467 : "execute purse deposit",
468 : TEH_MT_REQUEST_PURSE_DEPOSIT,
469 : &mhd_ret,
470 : &deposit_transaction,
471 : &pcc))
472 : {
473 4 : for (unsigned int i = 0; i<pcc.num_coins; i++)
474 2 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
475 2 : GNUNET_free (pcc.coins);
476 2 : return mhd_ret;
477 : }
478 : }
479 : {
480 5 : struct TALER_PurseEventP rep = {
481 5 : .header.size = htons (sizeof (rep)),
482 5 : .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED),
483 5 : .purse_pub = *pcc.purse_pub
484 : };
485 :
486 5 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
487 : "Notifying about purse deposit %s\n",
488 : TALER_B2S (pcc.purse_pub));
489 5 : TEH_plugin->event_notify (TEH_plugin->cls,
490 : &rep.header,
491 : NULL,
492 : 0);
493 : }
494 :
495 : /* generate regular response */
496 : {
497 : MHD_RESULT res;
498 :
499 5 : res = reply_deposit_success (connection,
500 : &pcc);
501 10 : for (unsigned int i = 0; i<pcc.num_coins; i++)
502 5 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
503 5 : GNUNET_free (pcc.coins);
504 5 : return res;
505 : }
506 : }
507 :
508 :
509 : /* end of taler-exchange-httpd_purses_deposit.c */
|