Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2020, 2021 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 :
20 : /**
21 : * @file taler-merchant-httpd_private-post-instances.c
22 : * @brief implementing POST /instances request handling
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd_private-post-instances.h"
27 : #include "taler-merchant-httpd_helper.h"
28 : #include <taler/taler_json_lib.h>
29 : #include <regex.h>
30 :
31 : /**
32 : * How often do we retry the simple INSERT database transaction?
33 : */
34 : #define MAX_RETRIES 3
35 :
36 :
37 : /**
38 : * Check if the array of @a payto_uris contains exactly the same
39 : * URIs as those already in @a mi (possibly in a different order).
40 : *
41 : * @param mi a merchant instance with accounts
42 : * @param payto_uris a JSON array with accounts (presumably)
43 : * @return true if they are 'equal', false if not or of payto_uris is not an array
44 : */
45 : static bool
46 0 : accounts_equal (const struct TMH_MerchantInstance *mi,
47 : json_t *payto_uris)
48 : {
49 0 : if (! json_is_array (payto_uris))
50 0 : return false;
51 0 : {
52 0 : unsigned int len = json_array_size (payto_uris);
53 0 : bool matches[GNUNET_NZL (len)];
54 : struct TMH_WireMethod *wm;
55 :
56 0 : memset (matches,
57 : 0,
58 0 : sizeof (matches));
59 0 : for (wm = mi->wm_head;
60 : NULL != wm;
61 0 : wm = wm->next)
62 : {
63 0 : const char *uri = wm->payto_uri;
64 :
65 0 : GNUNET_assert (NULL != uri);
66 0 : for (unsigned int i = 0; i<len; i++)
67 : {
68 0 : const char *str = json_string_value (json_array_get (payto_uris,
69 : i));
70 :
71 0 : GNUNET_assert (NULL != str);
72 0 : if (0 == strcasecmp (uri,
73 : str))
74 : {
75 0 : if (matches[i])
76 : {
77 0 : GNUNET_break (0);
78 0 : return false; /* duplicate entry!? */
79 : }
80 0 : matches[i] = true;
81 0 : break;
82 : }
83 : }
84 : }
85 0 : for (unsigned int i = 0; i<len; i++)
86 0 : if (! matches[i])
87 0 : return false;
88 : }
89 0 : return true;
90 : }
91 :
92 :
93 : /**
94 : * Free memory used by @a wm
95 : *
96 : * @param wm wire method to free
97 : */
98 : static void
99 0 : free_wm (struct TMH_WireMethod *wm)
100 : {
101 0 : GNUNET_free (wm->payto_uri);
102 0 : GNUNET_free (wm->wire_method);
103 0 : GNUNET_free (wm);
104 0 : }
105 :
106 :
107 : /**
108 : * Free memory used by @a mi.
109 : *
110 : * @param mi instance to free
111 : */
112 : static void
113 0 : free_mi (struct TMH_MerchantInstance *mi)
114 : {
115 : struct TMH_WireMethod *wm;
116 :
117 0 : while (NULL != (wm = mi->wm_head))
118 : {
119 0 : GNUNET_CONTAINER_DLL_remove (mi->wm_head,
120 : mi->wm_tail,
121 : wm);
122 0 : free_wm (wm);
123 : }
124 0 : GNUNET_free (mi->settings.id);
125 0 : GNUNET_free (mi->settings.name);
126 0 : GNUNET_free (mi->settings.website);
127 0 : GNUNET_free (mi->settings.email);
128 0 : GNUNET_free (mi->settings.logo);
129 0 : json_decref (mi->settings.address);
130 0 : json_decref (mi->settings.jurisdiction);
131 0 : GNUNET_free (mi);
132 0 : }
133 :
134 :
135 : /**
136 : * Generate an instance, given its configuration.
137 : *
138 : * @param rh context of the handler
139 : * @param connection the MHD connection to handle
140 : * @param[in,out] hc context with further information about the request
141 : * @return MHD result code
142 : */
143 : MHD_RESULT
144 0 : TMH_private_post_instances (const struct TMH_RequestHandler *rh,
145 : struct MHD_Connection *connection,
146 : struct TMH_HandlerContext *hc)
147 : {
148 : struct TALER_MERCHANTDB_InstanceSettings is;
149 : struct TALER_MERCHANTDB_InstanceAuthSettings ias;
150 : json_t *payto_uris;
151 0 : const char *auth_token = NULL;
152 0 : struct TMH_WireMethod *wm_head = NULL;
153 0 : struct TMH_WireMethod *wm_tail = NULL;
154 : json_t *jauth;
155 : struct GNUNET_JSON_Specification spec[] = {
156 0 : GNUNET_JSON_spec_json ("payto_uris",
157 : &payto_uris),
158 0 : GNUNET_JSON_spec_string ("id",
159 : (const char **) &is.id),
160 0 : GNUNET_JSON_spec_string ("name",
161 : (const char **) &is.name),
162 0 : GNUNET_JSON_spec_mark_optional(
163 : GNUNET_JSON_spec_string ("email",
164 : (const char **) &is.email),
165 : NULL),
166 0 : GNUNET_JSON_spec_mark_optional(
167 : GNUNET_JSON_spec_string ("website",
168 : (const char **) &is.website),
169 : NULL),
170 0 : GNUNET_JSON_spec_mark_optional(
171 : GNUNET_JSON_spec_string ("logo",
172 : (const char **) &is.logo),
173 : NULL),
174 0 : GNUNET_JSON_spec_json ("auth",
175 : &jauth),
176 0 : GNUNET_JSON_spec_json ("address",
177 : &is.address),
178 0 : GNUNET_JSON_spec_json ("jurisdiction",
179 : &is.jurisdiction),
180 0 : TALER_JSON_spec_amount ("default_max_wire_fee",
181 : TMH_currency,
182 : &is.default_max_wire_fee),
183 0 : GNUNET_JSON_spec_uint32 ("default_wire_fee_amortization",
184 : &is.default_wire_fee_amortization),
185 0 : TALER_JSON_spec_amount ("default_max_deposit_fee",
186 : TMH_currency,
187 : &is.default_max_deposit_fee),
188 0 : GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay",
189 : &is.default_wire_transfer_delay),
190 0 : GNUNET_JSON_spec_relative_time ("default_pay_delay",
191 : &is.default_pay_delay),
192 0 : GNUNET_JSON_spec_end ()
193 : };
194 :
195 : {
196 : enum GNUNET_GenericReturnValue res;
197 :
198 0 : res = TALER_MHD_parse_json_data (connection,
199 0 : hc->request_body,
200 : spec);
201 0 : if (GNUNET_OK != res)
202 : return (GNUNET_NO == res)
203 : ? MHD_YES
204 0 : : MHD_NO;
205 : }
206 :
207 : {
208 : enum GNUNET_GenericReturnValue ret;
209 :
210 0 : ret = TMH_check_auth_config (connection,
211 : jauth,
212 : &auth_token);
213 0 : if (GNUNET_OK != ret)
214 0 : return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
215 : }
216 :
217 : /* check payto_uris for well-formedness */
218 0 : if (! TMH_payto_uri_array_valid (payto_uris))
219 0 : return TALER_MHD_reply_with_error (connection,
220 : MHD_HTTP_BAD_REQUEST,
221 : TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
222 : NULL);
223 :
224 : /* check 'id' well-formed */
225 : {
226 : static bool once;
227 : static regex_t reg;
228 0 : bool id_wellformed = true;
229 :
230 0 : if (! once)
231 : {
232 0 : GNUNET_assert (0 ==
233 : regcomp (®,
234 : "^[A-Za-z0-9][A-Za-z0-9_.@-]+$",
235 : REG_EXTENDED));
236 : }
237 :
238 0 : if (0 != regexec (®,
239 0 : is.id,
240 : 0, NULL, 0))
241 0 : id_wellformed = false;
242 0 : if (! id_wellformed)
243 0 : return TALER_MHD_reply_with_error (connection,
244 : MHD_HTTP_BAD_REQUEST,
245 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
246 : "id");
247 : }
248 :
249 0 : if (! TMH_location_object_valid (is.address))
250 : {
251 0 : GNUNET_break_op (0);
252 0 : GNUNET_JSON_parse_free (spec);
253 0 : return TALER_MHD_reply_with_error (connection,
254 : MHD_HTTP_BAD_REQUEST,
255 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
256 : "address");
257 : }
258 :
259 0 : if (! TMH_location_object_valid (is.jurisdiction))
260 : {
261 0 : GNUNET_break_op (0);
262 0 : GNUNET_JSON_parse_free (spec);
263 0 : return TALER_MHD_reply_with_error (connection,
264 : MHD_HTTP_BAD_REQUEST,
265 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
266 : "jurisdiction");
267 : }
268 :
269 0 : if ( (NULL != is.logo) &&
270 0 : (! TMH_image_data_url_valid (is.logo)) )
271 : {
272 0 : GNUNET_break_op (0);
273 0 : GNUNET_JSON_parse_free (spec);
274 0 : return TALER_MHD_reply_with_error (connection,
275 : MHD_HTTP_BAD_REQUEST,
276 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
277 : "logo");
278 : }
279 :
280 : {
281 : /* Test if an instance of this id is known */
282 : struct TMH_MerchantInstance *mi;
283 :
284 0 : mi = TMH_lookup_instance (is.id);
285 0 : if (NULL != mi)
286 : {
287 0 : if (mi->deleted)
288 : {
289 0 : GNUNET_JSON_parse_free (spec);
290 0 : return TALER_MHD_reply_with_error (connection,
291 : MHD_HTTP_CONFLICT,
292 : TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED,
293 0 : is.id);
294 : }
295 : /* Check for idempotency */
296 0 : if ( (0 == strcmp (mi->settings.id,
297 0 : is.id)) &&
298 0 : (0 == strcmp (mi->settings.name,
299 0 : is.name)) &&
300 0 : ((mi->settings.email == is.email) ||
301 0 : (NULL != is.email && NULL != mi->settings.email &&
302 0 : 0 == strcmp (mi->settings.email,
303 0 : is.email))) &&
304 0 : ((mi->settings.website == is.website) ||
305 0 : (NULL != is.website && NULL != mi->settings.website &&
306 0 : 0 == strcmp (mi->settings.website,
307 0 : is.website))) &&
308 0 : ((mi->settings.logo == is.logo) ||
309 0 : (NULL != is.logo && NULL != mi->settings.logo &&
310 0 : 0 == strcmp (mi->settings.logo,
311 0 : is.logo))) &&
312 0 : ( ( (NULL != auth_token) &&
313 : (GNUNET_OK ==
314 0 : TMH_check_auth (auth_token,
315 : &mi->auth.auth_salt,
316 0 : &mi->auth.auth_hash)) ) ||
317 0 : ( (NULL == auth_token) &&
318 : (GNUNET_YES ==
319 0 : GNUNET_is_zero (&mi->auth.auth_hash))) ) &&
320 0 : (1 == json_equal (mi->settings.address,
321 0 : is.address)) &&
322 0 : (1 == json_equal (mi->settings.jurisdiction,
323 0 : is.jurisdiction)) &&
324 0 : (GNUNET_OK == TALER_amount_cmp_currency (
325 0 : &mi->settings.default_max_deposit_fee,
326 0 : &is.default_max_deposit_fee)) &&
327 0 : (0 == TALER_amount_cmp (&mi->settings.default_max_deposit_fee,
328 0 : &is.default_max_deposit_fee)) &&
329 0 : (GNUNET_OK == TALER_amount_cmp_currency (
330 0 : &mi->settings.default_max_wire_fee,
331 0 : &is.default_max_wire_fee)) &&
332 0 : (0 == TALER_amount_cmp (&mi->settings.default_max_wire_fee,
333 0 : &is.default_max_wire_fee)) &&
334 0 : (mi->settings.default_wire_fee_amortization ==
335 0 : is.default_wire_fee_amortization) &&
336 0 : (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay,
337 : ==,
338 0 : is.default_wire_transfer_delay)) &&
339 0 : (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay,
340 : ==,
341 0 : is.default_pay_delay)) &&
342 0 : (accounts_equal (mi,
343 : payto_uris)) )
344 : {
345 0 : GNUNET_JSON_parse_free (spec);
346 0 : return TALER_MHD_reply_static (connection,
347 : MHD_HTTP_NO_CONTENT,
348 : NULL,
349 : NULL,
350 : 0);
351 : }
352 : else
353 : {
354 0 : GNUNET_JSON_parse_free (spec);
355 0 : return TALER_MHD_reply_with_error (connection,
356 : MHD_HTTP_CONFLICT,
357 : TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS,
358 0 : is.id);
359 : }
360 : }
361 : }
362 :
363 : /* convert provided payto URIs into internal data structure with salts */
364 : {
365 0 : unsigned int len = json_array_size (payto_uris);
366 :
367 0 : for (unsigned int i = 0; i<len; i++)
368 : {
369 0 : json_t *payto_uri = json_array_get (payto_uris,
370 : i);
371 : struct TMH_WireMethod *wm;
372 :
373 0 : wm = TMH_setup_wire_account (json_string_value (payto_uri));
374 0 : GNUNET_CONTAINER_DLL_insert (wm_head,
375 : wm_tail,
376 : wm);
377 : }
378 : }
379 :
380 : /* handle authentication token setup */
381 0 : if (NULL == auth_token)
382 : {
383 0 : memset (&ias.auth_salt,
384 : 0,
385 : sizeof (ias.auth_salt));
386 0 : memset (&ias.auth_hash,
387 : 0,
388 : sizeof (ias.auth_hash));
389 : }
390 : else
391 : {
392 : /* Sets 'auth_salt' and 'auth_hash' */
393 0 : TMH_compute_auth (auth_token,
394 : &ias.auth_salt,
395 : &ias.auth_hash);
396 : }
397 :
398 : /* create in-memory data structure */
399 : {
400 : struct TMH_MerchantInstance *mi;
401 : enum GNUNET_DB_QueryStatus qs;
402 :
403 0 : mi = GNUNET_new (struct TMH_MerchantInstance);
404 0 : mi->wm_head = wm_head;
405 0 : mi->wm_tail = wm_tail;
406 0 : mi->settings = is;
407 0 : mi->settings.address = json_incref (mi->settings.address);
408 0 : mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
409 0 : mi->settings.id = GNUNET_strdup (is.id);
410 0 : mi->settings.name = GNUNET_strdup (is.name);
411 0 : if (NULL != is.email)
412 0 : mi->settings.email = GNUNET_strdup (is.email);
413 0 : if (NULL != is.website)
414 0 : mi->settings.website = GNUNET_strdup (is.website);
415 0 : if (NULL != is.logo)
416 0 : mi->settings.logo = GNUNET_strdup (is.logo);
417 0 : mi->auth = ias;
418 0 : GNUNET_CRYPTO_eddsa_key_create (&mi->merchant_priv.eddsa_priv);
419 0 : GNUNET_CRYPTO_eddsa_key_get_public (&mi->merchant_priv.eddsa_priv,
420 : &mi->merchant_pub.eddsa_pub);
421 :
422 0 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
423 : {
424 0 : if (GNUNET_OK !=
425 0 : TMH_db->start (TMH_db->cls,
426 : "post /instances"))
427 : {
428 0 : GNUNET_JSON_parse_free (spec);
429 0 : free_mi (mi);
430 0 : return TALER_MHD_reply_with_error (connection,
431 : MHD_HTTP_INTERNAL_SERVER_ERROR,
432 : TALER_EC_GENERIC_DB_START_FAILED,
433 : NULL);
434 : }
435 0 : qs = TMH_db->insert_instance (TMH_db->cls,
436 0 : &mi->merchant_pub,
437 0 : &mi->merchant_priv,
438 0 : &mi->settings,
439 0 : &mi->auth);
440 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
441 : {
442 : MHD_RESULT ret;
443 :
444 0 : TMH_db->rollback (TMH_db->cls);
445 0 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
446 0 : goto retry;
447 0 : ret = TALER_MHD_reply_with_error (connection,
448 : MHD_HTTP_CONFLICT,
449 : TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS,
450 0 : is.id);
451 0 : GNUNET_JSON_parse_free (spec);
452 0 : free_mi (mi);
453 0 : return ret;
454 : }
455 0 : for (struct TMH_WireMethod *wm = wm_head;
456 : NULL != wm;
457 0 : wm = wm->next)
458 : {
459 0 : struct TALER_MERCHANTDB_AccountDetails ad = {
460 0 : .payto_uri = wm->payto_uri,
461 : .salt = wm->wire_salt,
462 : .h_wire = wm->h_wire,
463 0 : .active = wm->active
464 : };
465 :
466 0 : qs = TMH_db->insert_account (TMH_db->cls,
467 0 : mi->settings.id,
468 : &ad);
469 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
470 0 : break;
471 : }
472 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
473 : {
474 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
475 0 : TMH_db->rollback (TMH_db->cls);
476 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
477 0 : break;
478 0 : goto retry;
479 : }
480 0 : qs = TMH_db->commit (TMH_db->cls);
481 0 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
482 0 : qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
483 0 : retry:
484 0 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
485 0 : break; /* success! -- or hard failure */
486 : } /* for .. MAX_RETRIES */
487 0 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
488 : {
489 0 : GNUNET_JSON_parse_free (spec);
490 0 : free_mi (mi);
491 0 : return TALER_MHD_reply_with_error (connection,
492 : MHD_HTTP_INTERNAL_SERVER_ERROR,
493 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
494 : NULL);
495 : }
496 : /* Finally, also update our running process */
497 0 : GNUNET_assert (GNUNET_OK ==
498 : TMH_add_instance (mi));
499 0 : TMH_reload_instances (mi->settings.id);
500 : }
501 0 : GNUNET_JSON_parse_free (spec);
502 0 : if (0 == strcmp (is.id,
503 : "default"))
504 : {
505 0 : GNUNET_free (TMH_default_auth); /* clear it if the default instance was
506 : created */
507 : }
508 :
509 0 : return TALER_MHD_reply_static (connection,
510 : MHD_HTTP_NO_CONTENT,
511 : NULL,
512 : NULL,
513 : 0);
514 : }
515 :
516 :
517 : /* end of taler-merchant-httpd_private-post-instances.c */
|