Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 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 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 <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file src/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-collect.c
18 : * @brief Handle request to collect a zero-Taler order without a wallet
19 : * @author Bohdan Potuzhnyi
20 : * @author Volodymyr Potuzhnyi
21 : */
22 : #include "platform.h"
23 : #include <jansson.h>
24 : #include <taler/taler_json_lib.h>
25 : #include <taler/taler_merchant_util.h>
26 : #include "taler-merchant-httpd_post-private-orders-ORDER_ID-collect.h"
27 : #include "taler-merchant-httpd_post-orders-ORDER_ID-claim.h"
28 : #include "taler-merchant-httpd_post-orders-ORDER_ID-pay.h"
29 : #include "merchant-database/get_contract_terms.h"
30 : #include "merchant-database/get_order.h"
31 : #include "merchant-database/preflight.h"
32 :
33 :
34 : /**
35 : * Outcome of checking whether an order may be collected.
36 : */
37 : enum CollectCheck
38 : {
39 :
40 : /**
41 : * The contract terms could not be parsed, or use a contract version
42 : * we do not know. This is about the contract we stored ourselves,
43 : * so it indicates a problem on our side, not a bad request.
44 : */
45 : COLLECT_CHECK_INVALID = 0,
46 :
47 : /**
48 : * The order can be collected: nothing remains to be paid with
49 : * coins, so the payment logic will find an empty balance to settle.
50 : */
51 : COLLECT_CHECK_OK,
52 :
53 : /**
54 : * The order is not free on the Taler side, so completing it needs a
55 : * wallet and the merchant must not do it on the customer's behalf.
56 : * "Free" here means all of: the amount to be paid over Taler is
57 : * zero (the rest being covered by @e amount_external), the choice
58 : * consumes no token @e inputs, and it yields no token @e outputs.
59 : */
60 : COLLECT_CHECK_NOT_FREE,
61 :
62 : /**
63 : * The contract is v1 and thus offers several choices, but the
64 : * client did not say which one to collect. We do not pick one on
65 : * the client's behalf, just as the backend does not pick one on the
66 : * wallet's behalf when paying normally.
67 : */
68 : COLLECT_CHECK_CHOICE_MISSING,
69 :
70 : /**
71 : * A choice was given that the contract does not offer: either an
72 : * index beyond the end of the v1 @e choices array, or any index at
73 : * all for a v0 contract, which has no choices to select from.
74 : */
75 : COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS
76 :
77 : };
78 :
79 :
80 : /**
81 : * How often do we retry the database transaction?
82 : */
83 : #define MAX_RETRIES 3
84 :
85 :
86 : /**
87 : * Derive the deterministic nonce this backend uses to claim
88 : * @a order_id itself. Using a deterministic nonce allows us to
89 : * distinguish orders we claimed via collect from orders claimed
90 : * by a customer wallet, and makes the collect operation
91 : * idempotent.
92 : *
93 : * @param hc handler context with the instance public key
94 : * @param order_id order the nonce is for
95 : * @param[out] nonce set to the derived nonce
96 : */
97 : static void
98 10 : derive_collect_nonce (const struct TMH_HandlerContext *hc,
99 : const char *order_id,
100 : struct GNUNET_CRYPTO_EddsaPublicKey *nonce)
101 : {
102 10 : GNUNET_assert (GNUNET_YES ==
103 : GNUNET_CRYPTO_hkdf_gnunet (
104 : nonce,
105 : sizeof (*nonce),
106 : order_id,
107 : strlen (order_id),
108 : &hc->instance->merchant_pub,
109 : sizeof (hc->instance->merchant_pub)));
110 10 : }
111 :
112 :
113 : /**
114 : * Check that @a contract_terms describe an order the backend may
115 : * complete on its own: a genuinely free Taler payment.
116 : *
117 : * The payment logic we hand the order to assumes it is paid with
118 : * coins; handing it an order that actually costs something would make
119 : * it fail in a way that says nothing useful, so we filter those out
120 : * before claiming anything.
121 : *
122 : * @param contract_terms contract terms to check
123 : * @param choice_index choice selected by the client, -1 if none was given
124 : * @return #COLLECT_CHECK_OK if the order can be collected
125 : */
126 : static enum CollectCheck
127 18 : check_collectable (const json_t *contract_terms,
128 : int16_t choice_index)
129 : {
130 18 : enum TALER_MERCHANT_ContractVersion version
131 : = TALER_MERCHANT_CONTRACT_VERSION_0;
132 : struct GNUNET_JSON_Specification spec[] = {
133 18 : GNUNET_JSON_spec_mark_optional (
134 : TALER_MERCHANT_spec_contract_version ("version",
135 : &version),
136 : NULL),
137 18 : GNUNET_JSON_spec_end ()
138 : };
139 :
140 18 : if (GNUNET_OK !=
141 18 : GNUNET_JSON_parse (contract_terms,
142 : spec,
143 : NULL,
144 : NULL))
145 : {
146 0 : GNUNET_break (0);
147 0 : return COLLECT_CHECK_INVALID;
148 : }
149 18 : switch (version)
150 : {
151 10 : case TALER_MERCHANT_CONTRACT_VERSION_0:
152 : {
153 : struct TALER_Amount amount;
154 : struct GNUNET_JSON_Specification aspec[] = {
155 10 : TALER_JSON_spec_amount_any ("amount",
156 : &amount),
157 10 : GNUNET_JSON_spec_end ()
158 : };
159 :
160 10 : if (GNUNET_OK !=
161 10 : GNUNET_JSON_parse (contract_terms,
162 : aspec,
163 : NULL,
164 : NULL))
165 : {
166 0 : GNUNET_break (0);
167 0 : return COLLECT_CHECK_INVALID;
168 : }
169 10 : if (0 <= choice_index)
170 : {
171 : /* v0 contracts have no choices to select from */
172 2 : GNUNET_break_op (0);
173 2 : return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
174 : }
175 8 : if (! TALER_amount_is_zero (&amount))
176 2 : return COLLECT_CHECK_NOT_FREE;
177 6 : return COLLECT_CHECK_OK;
178 : }
179 8 : case TALER_MERCHANT_CONTRACT_VERSION_1:
180 : {
181 : const json_t *choice;
182 : struct TALER_Amount amount;
183 :
184 8 : if (0 > choice_index)
185 : {
186 : /* Which choice to complete is the client's decision, just as
187 : it is the wallet's decision when paying normally. */
188 2 : GNUNET_break_op (0);
189 2 : return COLLECT_CHECK_CHOICE_MISSING;
190 : }
191 6 : choice = json_array_get (json_object_get (contract_terms,
192 : "choices"),
193 : (size_t) choice_index);
194 6 : if (NULL == choice)
195 : {
196 2 : GNUNET_break_op (0);
197 2 : return COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS;
198 : }
199 : {
200 : struct GNUNET_JSON_Specification cspec[] = {
201 4 : TALER_JSON_spec_amount_any ("amount",
202 : &amount),
203 4 : GNUNET_JSON_spec_end ()
204 : };
205 :
206 4 : if (GNUNET_OK !=
207 4 : GNUNET_JSON_parse (choice,
208 : cspec,
209 : NULL,
210 : NULL))
211 : {
212 0 : GNUNET_break (0);
213 0 : return COLLECT_CHECK_INVALID;
214 : }
215 : }
216 4 : if (! TALER_amount_is_zero (&amount))
217 0 : return COLLECT_CHECK_NOT_FREE;
218 4 : if (0 != json_array_size (json_object_get (choice,
219 : "inputs")))
220 0 : return COLLECT_CHECK_NOT_FREE;
221 4 : if (0 != json_array_size (json_object_get (choice,
222 : "outputs")))
223 0 : return COLLECT_CHECK_NOT_FREE;
224 4 : return COLLECT_CHECK_OK;
225 : }
226 : }
227 0 : GNUNET_break (0);
228 0 : return COLLECT_CHECK_INVALID;
229 : }
230 :
231 :
232 : enum MHD_Result
233 20 : TMH_private_post_orders_ID_collect (const struct TMH_RequestHandler *rh,
234 : struct MHD_Connection *connection,
235 : struct TMH_HandlerContext *hc)
236 : {
237 20 : const char *order_id = hc->infix;
238 20 : const char *session_id = NULL;
239 20 : int16_t choice_index = -1;
240 : struct GNUNET_CRYPTO_EddsaPublicKey nonce;
241 20 : struct TALER_ClaimTokenP order_ct = { 0 };
242 20 : json_t *contract_terms = NULL;
243 : enum GNUNET_DB_QueryStatus qs;
244 :
245 20 : if (NULL != hc->ctx)
246 : {
247 : /* We already handed this request over to the payment logic and
248 : were resumed; let it continue where it left off. */
249 0 : return TMH_post_orders_ID_pay (rh,
250 : connection,
251 : hc);
252 : }
253 20 : if (NULL != hc->request_body)
254 : {
255 : struct GNUNET_JSON_Specification spec[] = {
256 20 : GNUNET_JSON_spec_mark_optional (
257 : GNUNET_JSON_spec_string ("session_id",
258 : &session_id),
259 : NULL),
260 20 : GNUNET_JSON_spec_mark_optional (
261 : GNUNET_JSON_spec_int16 ("choice_index",
262 : &choice_index),
263 : NULL),
264 20 : GNUNET_JSON_spec_end ()
265 : };
266 : enum GNUNET_GenericReturnValue res;
267 :
268 20 : res = TALER_MHD_parse_json_data (connection,
269 20 : hc->request_body,
270 : spec);
271 20 : if (GNUNET_OK != res)
272 : {
273 0 : GNUNET_break_op (0);
274 : return (GNUNET_NO == res)
275 : ? MHD_YES
276 0 : : MHD_NO;
277 : }
278 : }
279 :
280 : /* Pre-filter: only genuinely free orders may be completed without a
281 : wallet, and the client has to say which choice to complete. */
282 : {
283 20 : json_t *order_terms = NULL;
284 : uint64_t order_serial;
285 :
286 20 : TALER_MERCHANTDB_preflight (TMH_db);
287 20 : qs = TALER_MERCHANTDB_get_contract_terms (TMH_db,
288 20 : hc->instance->settings.id,
289 : order_id,
290 : &order_terms,
291 : &order_serial,
292 : NULL);
293 20 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
294 : {
295 : struct TALER_MerchantPostDataHashP unused;
296 :
297 : /* Remember the order's own claim token: as the merchant we may
298 : present it ourselves when claiming the order below. */
299 14 : qs = TALER_MERCHANTDB_get_order (TMH_db,
300 14 : hc->instance->settings.id,
301 : order_id,
302 : &order_ct,
303 : &unused,
304 : &order_terms);
305 : }
306 20 : if (0 > qs)
307 10 : return TALER_MHD_reply_with_error (connection,
308 : MHD_HTTP_INTERNAL_SERVER_ERROR,
309 : TALER_EC_GENERIC_DB_FETCH_FAILED,
310 : "get order for collection");
311 20 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
312 2 : return TALER_MHD_reply_with_error (connection,
313 : MHD_HTTP_NOT_FOUND,
314 : TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
315 : order_id);
316 : {
317 : enum CollectCheck cc;
318 :
319 18 : cc = check_collectable (order_terms,
320 : choice_index);
321 18 : json_decref (order_terms);
322 18 : switch (cc)
323 : {
324 10 : case COLLECT_CHECK_OK:
325 10 : break;
326 2 : case COLLECT_CHECK_NOT_FREE:
327 2 : return TALER_MHD_reply_with_error (
328 : connection,
329 : MHD_HTTP_CONFLICT,
330 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_NOT_FREE,
331 : order_id);
332 2 : case COLLECT_CHECK_CHOICE_MISSING:
333 2 : return TALER_MHD_reply_with_error (
334 : connection,
335 : MHD_HTTP_BAD_REQUEST,
336 : TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING,
337 : order_id);
338 4 : case COLLECT_CHECK_CHOICE_OUT_OF_BOUNDS:
339 4 : return TALER_MHD_reply_with_error (
340 : connection,
341 : MHD_HTTP_BAD_REQUEST,
342 : TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS,
343 : order_id);
344 0 : case COLLECT_CHECK_INVALID:
345 0 : return TALER_MHD_reply_with_error (
346 : connection,
347 : MHD_HTTP_INTERNAL_SERVER_ERROR,
348 : TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
349 : order_id);
350 : }
351 : }
352 : }
353 :
354 : /* Claim the order for ourselves */
355 10 : derive_collect_nonce (hc,
356 : order_id,
357 : &nonce);
358 10 : for (unsigned int i=0; i<MAX_RETRIES; i++)
359 : {
360 10 : TALER_MERCHANTDB_preflight (TMH_db);
361 10 : qs = TMH_claim_order (hc,
362 : order_id,
363 : &nonce,
364 : &order_ct,
365 : &contract_terms);
366 10 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
367 10 : break;
368 : }
369 10 : switch (qs)
370 : {
371 0 : case GNUNET_DB_STATUS_HARD_ERROR:
372 0 : return TALER_MHD_reply_with_error (connection,
373 : MHD_HTTP_INTERNAL_SERVER_ERROR,
374 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
375 : NULL);
376 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
377 0 : return TALER_MHD_reply_with_error (connection,
378 : MHD_HTTP_INTERNAL_SERVER_ERROR,
379 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
380 : NULL);
381 2 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
382 2 : if (NULL == contract_terms)
383 0 : return TALER_MHD_reply_with_error (connection,
384 : MHD_HTTP_NOT_FOUND,
385 : TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
386 : order_id);
387 : /* Claimed by a customer wallet: the wallet owns the order and has
388 : to execute the payment itself. */
389 2 : json_decref (contract_terms);
390 2 : return TALER_MHD_reply_with_error (
391 : connection,
392 : MHD_HTTP_CONFLICT,
393 : TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_ALREADY_CLAIMED,
394 : order_id);
395 8 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
396 8 : GNUNET_assert (NULL != contract_terms);
397 8 : json_decref (contract_terms);
398 8 : break;
399 : }
400 :
401 : /* Turn our request into the payment request a wallet would send for
402 : a free order and let the regular payment logic handle it, so that
403 : collecting an order behaves exactly like paying it. */
404 : {
405 : json_t *pay_request;
406 :
407 8 : pay_request = GNUNET_JSON_PACK (
408 : GNUNET_JSON_pack_array_steal ("coins",
409 : json_array ()),
410 : GNUNET_JSON_pack_allow_null (
411 : GNUNET_JSON_pack_string ("session_id",
412 : session_id)));
413 8 : GNUNET_assert (NULL != pay_request);
414 8 : if (0 <= choice_index)
415 4 : GNUNET_assert (0 ==
416 : json_object_set_new (
417 : pay_request,
418 : "wallet_data",
419 : GNUNET_JSON_PACK (
420 : GNUNET_JSON_pack_int64 ("choice_index",
421 : choice_index))));
422 8 : if (NULL != hc->request_body)
423 8 : json_decref (hc->request_body);
424 8 : hc->request_body = pay_request;
425 : }
426 8 : return TMH_post_orders_ID_pay (rh,
427 : connection,
428 : hc);
429 : }
430 :
431 :
432 : /* end of taler-merchant-httpd_post-private-orders-ORDER_ID-collect.c */
|