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 "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_dbevents.h"
29 : #include "taler_json_lib.h"
30 : #include "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_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 0 : 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 0 : if (TALER_EC_NONE !=
106 0 : (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 : 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 0 : 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 0 : deposit_transaction (void *cls,
157 : struct MHD_Connection *connection,
158 : MHD_RESULT *mhd_ret)
159 : {
160 0 : struct PurseDepositContext *pcc = cls;
161 : enum GNUNET_DB_QueryStatus qs;
162 :
163 0 : qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
164 0 : for (unsigned int i = 0; i<pcc->num_coins; i++)
165 : {
166 0 : struct TEH_PurseDepositedCoin *coin = &pcc->coins[i];
167 0 : bool balance_ok = false;
168 0 : bool conflict = true;
169 :
170 0 : qs = TEH_make_coin_known (&coin->cpi,
171 : connection,
172 : &coin->known_coin_id,
173 : mhd_ret);
174 0 : if (qs < 0)
175 0 : return qs;
176 0 : qs = TEH_plugin->do_purse_deposit (TEH_plugin->cls,
177 : pcc->purse_pub,
178 0 : &coin->cpi.coin_pub,
179 0 : &coin->amount,
180 0 : &coin->coin_sig,
181 0 : &coin->amount_minus_fee,
182 : &balance_ok,
183 : &conflict);
184 0 : if (qs <= 0)
185 : {
186 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
187 0 : return qs;
188 0 : TALER_LOG_WARNING (
189 : "Failed to store purse deposit information in database\n");
190 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
191 : MHD_HTTP_INTERNAL_SERVER_ERROR,
192 : TALER_EC_GENERIC_DB_STORE_FAILED,
193 : "do purse deposit");
194 0 : return qs;
195 : }
196 0 : if (! balance_ok)
197 : {
198 : *mhd_ret
199 0 : = TEH_RESPONSE_reply_coin_insufficient_funds (
200 : connection,
201 : TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS,
202 0 : &coin->cpi.denom_pub_hash,
203 0 : &coin->cpi.coin_pub);
204 0 : return GNUNET_DB_STATUS_HARD_ERROR;
205 : }
206 0 : if (conflict)
207 : {
208 : struct TALER_Amount amount;
209 : struct TALER_CoinSpendPublicKeyP coin_pub;
210 : struct TALER_CoinSpendSignatureP coin_sig;
211 : struct TALER_DenominationHashP h_denom_pub;
212 : struct TALER_AgeCommitmentHash phac;
213 0 : char *partner_url = NULL;
214 :
215 0 : TEH_plugin->rollback (TEH_plugin->cls);
216 0 : qs = TEH_plugin->get_purse_deposit (TEH_plugin->cls,
217 : pcc->purse_pub,
218 0 : &coin->cpi.coin_pub,
219 : &amount,
220 : &h_denom_pub,
221 : &phac,
222 : &coin_sig,
223 : &partner_url);
224 0 : if (qs < 0)
225 : {
226 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
227 0 : TALER_LOG_WARNING (
228 : "Failed to fetch purse deposit information from database\n");
229 0 : *mhd_ret = TALER_MHD_reply_with_error (connection,
230 : MHD_HTTP_INTERNAL_SERVER_ERROR,
231 : TALER_EC_GENERIC_DB_FETCH_FAILED,
232 : "get purse deposit");
233 0 : return GNUNET_DB_STATUS_HARD_ERROR;
234 : }
235 :
236 : *mhd_ret
237 0 : = TALER_MHD_REPLY_JSON_PACK (
238 : connection,
239 : MHD_HTTP_CONFLICT,
240 : TALER_JSON_pack_ec (
241 : TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA),
242 : GNUNET_JSON_pack_data_auto ("coin_pub",
243 : &coin_pub),
244 : GNUNET_JSON_pack_data_auto ("h_denom_pub",
245 : &h_denom_pub),
246 : GNUNET_JSON_pack_data_auto ("h_age_commitment",
247 : &phac),
248 : GNUNET_JSON_pack_data_auto ("coin_sig",
249 : &coin_sig),
250 : GNUNET_JSON_pack_allow_null (
251 : GNUNET_JSON_pack_string ("partner_url",
252 : partner_url)),
253 : TALER_JSON_pack_amount ("amount",
254 : &amount));
255 0 : GNUNET_free (partner_url);
256 0 : return GNUNET_DB_STATUS_HARD_ERROR;
257 : }
258 : }
259 0 : return qs;
260 : }
261 :
262 :
263 : /**
264 : * Parse a coin and check signature of the coin and the denomination
265 : * signature over the coin.
266 : *
267 : * @param[in,out] connection our HTTP connection
268 : * @param[in,out] pcc request context
269 : * @param[out] coin coin to initialize
270 : * @param jcoin coin to parse
271 : * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned,
272 : * #GNUNET_SYSERR on failure and no error could be returned
273 : */
274 : static enum GNUNET_GenericReturnValue
275 0 : parse_coin (struct MHD_Connection *connection,
276 : struct PurseDepositContext *pcc,
277 : struct TEH_PurseDepositedCoin *coin,
278 : const json_t *jcoin)
279 : {
280 : enum GNUNET_GenericReturnValue iret;
281 :
282 0 : if (GNUNET_OK !=
283 0 : (iret = TEH_common_purse_deposit_parse_coin (connection,
284 : coin,
285 : jcoin)))
286 0 : return iret;
287 0 : if (GNUNET_OK !=
288 0 : (iret = TEH_common_deposit_check_purse_deposit (
289 : connection,
290 : coin,
291 : pcc->purse_pub,
292 : pcc->min_age)))
293 0 : return iret;
294 0 : if (0 >
295 0 : TALER_amount_add (&pcc->deposit_total,
296 0 : &pcc->deposit_total,
297 0 : &coin->amount_minus_fee))
298 : {
299 0 : GNUNET_break (0);
300 0 : return TALER_MHD_reply_with_error (connection,
301 : MHD_HTTP_INTERNAL_SERVER_ERROR,
302 : TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
303 : "total deposit contribution");
304 : }
305 0 : return GNUNET_OK;
306 : }
307 :
308 :
309 : MHD_RESULT
310 0 : TEH_handler_purses_deposit (
311 : struct MHD_Connection *connection,
312 : const struct TALER_PurseContractPublicKeyP *purse_pub,
313 : const json_t *root)
314 : {
315 0 : struct PurseDepositContext pcc = {
316 : .purse_pub = purse_pub,
317 0 : .exchange_timestamp = GNUNET_TIME_timestamp_get ()
318 : };
319 : json_t *deposits;
320 : json_t *deposit;
321 : unsigned int idx;
322 : struct GNUNET_JSON_Specification spec[] = {
323 0 : GNUNET_JSON_spec_json ("deposits",
324 : &deposits),
325 0 : GNUNET_JSON_spec_end ()
326 : };
327 :
328 : {
329 : enum GNUNET_GenericReturnValue res;
330 :
331 0 : res = TALER_MHD_parse_json_data (connection,
332 : root,
333 : spec);
334 0 : if (GNUNET_SYSERR == res)
335 : {
336 0 : GNUNET_break (0);
337 0 : return MHD_NO; /* hard failure */
338 : }
339 0 : if (GNUNET_NO == res)
340 : {
341 0 : GNUNET_break_op (0);
342 0 : return MHD_YES; /* failure */
343 : }
344 : }
345 0 : GNUNET_assert (GNUNET_OK ==
346 : TALER_amount_set_zero (TEH_currency,
347 : &pcc.deposit_total));
348 0 : pcc.num_coins = json_array_size (deposits);
349 0 : if ( (0 == pcc.num_coins) ||
350 0 : (pcc.num_coins > TALER_MAX_FRESH_COINS) )
351 : {
352 0 : GNUNET_break_op (0);
353 0 : GNUNET_JSON_parse_free (spec);
354 0 : return TALER_MHD_reply_with_error (connection,
355 : MHD_HTTP_BAD_REQUEST,
356 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
357 : "deposits");
358 : }
359 :
360 : {
361 : enum GNUNET_DB_QueryStatus qs;
362 : struct GNUNET_TIME_Timestamp merge_timestamp;
363 :
364 0 : qs = TEH_plugin->select_purse (
365 0 : TEH_plugin->cls,
366 : pcc.purse_pub,
367 : &pcc.purse_expiration,
368 : &pcc.amount,
369 : &pcc.deposit_total,
370 : &pcc.h_contract_terms,
371 : &merge_timestamp);
372 0 : switch (qs)
373 : {
374 0 : case GNUNET_DB_STATUS_HARD_ERROR:
375 0 : GNUNET_break (0);
376 0 : return TALER_MHD_reply_with_error (connection,
377 : MHD_HTTP_INTERNAL_SERVER_ERROR,
378 : TALER_EC_GENERIC_DB_FETCH_FAILED,
379 : "select purse");
380 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
381 0 : GNUNET_break (0);
382 0 : return TALER_MHD_reply_with_error (connection,
383 : MHD_HTTP_INTERNAL_SERVER_ERROR,
384 : TALER_EC_GENERIC_DB_FETCH_FAILED,
385 : "select purse");
386 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
387 0 : return TALER_MHD_reply_with_error (connection,
388 : MHD_HTTP_NOT_FOUND,
389 : TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN,
390 : NULL);
391 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
392 0 : break; /* handled below */
393 : }
394 0 : if (GNUNET_TIME_absolute_is_past (pcc.purse_expiration.abs_time))
395 : {
396 0 : return TALER_MHD_reply_with_error (connection,
397 : MHD_HTTP_GONE,
398 : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED,
399 : NULL);
400 : }
401 : }
402 :
403 : /* parse deposits */
404 0 : pcc.coins = GNUNET_new_array (pcc.num_coins,
405 : struct TEH_PurseDepositedCoin);
406 0 : json_array_foreach (deposits, idx, deposit)
407 : {
408 : enum GNUNET_GenericReturnValue res;
409 0 : struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx];
410 :
411 0 : res = parse_coin (connection,
412 : &pcc,
413 : coin,
414 : deposit);
415 0 : if (GNUNET_OK != res)
416 : {
417 0 : GNUNET_JSON_parse_free (spec);
418 0 : for (unsigned int i = 0; i<idx; i++)
419 0 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
420 0 : GNUNET_free (pcc.coins);
421 0 : return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
422 : }
423 : }
424 :
425 0 : if (GNUNET_SYSERR ==
426 0 : TEH_plugin->preflight (TEH_plugin->cls))
427 : {
428 0 : GNUNET_break (0);
429 0 : GNUNET_JSON_parse_free (spec);
430 0 : for (unsigned int i = 0; i<pcc.num_coins; i++)
431 0 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
432 0 : GNUNET_free (pcc.coins);
433 0 : return TALER_MHD_reply_with_error (connection,
434 : MHD_HTTP_INTERNAL_SERVER_ERROR,
435 : TALER_EC_GENERIC_DB_START_FAILED,
436 : "preflight failure");
437 : }
438 :
439 : /* execute transaction */
440 : {
441 : MHD_RESULT mhd_ret;
442 :
443 0 : if (GNUNET_OK !=
444 0 : TEH_DB_run_transaction (connection,
445 : "execute purse deposit",
446 : TEH_MT_REQUEST_PURSE_DEPOSIT,
447 : &mhd_ret,
448 : &deposit_transaction,
449 : &pcc))
450 : {
451 0 : GNUNET_JSON_parse_free (spec);
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 mhd_ret;
456 : }
457 : }
458 : {
459 0 : struct TALER_PurseEventP rep = {
460 0 : .header.size = htons (sizeof (rep)),
461 0 : .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED),
462 0 : .purse_pub = *pcc.purse_pub
463 : };
464 :
465 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
466 : "Notifying about purse deposit %s\n",
467 : TALER_B2S (pcc.purse_pub));
468 0 : TEH_plugin->event_notify (TEH_plugin->cls,
469 : &rep.header,
470 : NULL,
471 : 0);
472 : }
473 :
474 : /* generate regular response */
475 : {
476 : MHD_RESULT res;
477 :
478 0 : res = reply_deposit_success (connection,
479 : &pcc);
480 0 : for (unsigned int i = 0; i<pcc.num_coins; i++)
481 0 : TEH_common_purse_deposit_free_coin (&pcc.coins[i]);
482 0 : GNUNET_free (pcc.coins);
483 0 : GNUNET_JSON_parse_free (spec);
484 0 : return res;
485 : }
486 : }
487 :
488 :
489 : /* end of taler-exchange-httpd_purses_deposit.c */
|