taler-merchant-httpd_private-post-instances.c (23152B)
1 /* 2 This file is part of TALER 3 (C) 2020-2025 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-merchant-httpd.h" 29 #include "taler-merchant-httpd_mfa.h" 30 #include "taler_merchant_bank_lib.h" 31 #include <taler/taler_dbevents.h> 32 #include <taler/taler_json_lib.h> 33 #include <regex.h> 34 35 /** 36 * How often do we retry the simple INSERT database transaction? 37 */ 38 #define MAX_RETRIES 3 39 40 41 /** 42 * Generate an instance, given its configuration. 43 * 44 * @param rh context of the handler 45 * @param connection the MHD connection to handle 46 * @param[in,out] hc context with further information about the request 47 * @param login_token_expiration set to how long a login token validity 48 * should be, use zero if no login token should be created 49 * @param validation_needed true if self-provisioned and 50 * email/phone registration is required before the 51 * instance can become fully active 52 * @return MHD result code 53 */ 54 static MHD_RESULT 55 post_instances (const struct TMH_RequestHandler *rh, 56 struct MHD_Connection *connection, 57 struct TMH_HandlerContext *hc, 58 struct GNUNET_TIME_Relative login_token_expiration, 59 bool validation_needed) 60 { 61 struct TALER_MERCHANTDB_InstanceSettings is = { 0 }; 62 struct TALER_MERCHANTDB_InstanceAuthSettings ias; 63 const char *auth_password = NULL; 64 struct TMH_WireMethod *wm_head = NULL; 65 struct TMH_WireMethod *wm_tail = NULL; 66 const json_t *jauth; 67 bool no_pay_delay; 68 bool no_refund_delay; 69 bool no_transfer_delay; 70 bool no_rounding_interval; 71 struct GNUNET_JSON_Specification spec[] = { 72 GNUNET_JSON_spec_string ("id", 73 (const char **) &is.id), 74 GNUNET_JSON_spec_string ("name", 75 (const char **) &is.name), 76 GNUNET_JSON_spec_mark_optional ( 77 GNUNET_JSON_spec_string ("email", 78 (const char **) &is.email), 79 NULL), 80 GNUNET_JSON_spec_mark_optional ( 81 GNUNET_JSON_spec_string ("phone_number", 82 (const char **) &is.phone), 83 NULL), 84 GNUNET_JSON_spec_mark_optional ( 85 GNUNET_JSON_spec_string ("website", 86 (const char **) &is.website), 87 NULL), 88 GNUNET_JSON_spec_mark_optional ( 89 GNUNET_JSON_spec_string ("logo", 90 (const char **) &is.logo), 91 NULL), 92 GNUNET_JSON_spec_object_const ("auth", 93 &jauth), 94 GNUNET_JSON_spec_json ("address", 95 &is.address), 96 GNUNET_JSON_spec_json ("jurisdiction", 97 &is.jurisdiction), 98 GNUNET_JSON_spec_bool ("use_stefan", 99 &is.use_stefan), 100 GNUNET_JSON_spec_mark_optional ( 101 GNUNET_JSON_spec_relative_time ("default_pay_delay", 102 &is.default_pay_delay), 103 &no_pay_delay), 104 GNUNET_JSON_spec_mark_optional ( 105 GNUNET_JSON_spec_relative_time ("default_refund_delay", 106 &is.default_refund_delay), 107 &no_refund_delay), 108 GNUNET_JSON_spec_mark_optional ( 109 GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", 110 &is.default_wire_transfer_delay), 111 &no_transfer_delay), 112 GNUNET_JSON_spec_mark_optional ( 113 GNUNET_JSON_spec_time_rounder_interval ( 114 "default_wire_transfer_rounding_interval", 115 &is.default_wire_transfer_rounding_interval), 116 &no_rounding_interval), 117 GNUNET_JSON_spec_end () 118 }; 119 120 { 121 enum GNUNET_GenericReturnValue res; 122 123 res = TALER_MHD_parse_json_data (connection, 124 hc->request_body, 125 spec); 126 if (GNUNET_OK != res) 127 return (GNUNET_NO == res) 128 ? MHD_YES 129 : MHD_NO; 130 } 131 if (no_pay_delay) 132 is.default_pay_delay = TMH_default_pay_delay; 133 if (no_refund_delay) 134 is.default_refund_delay = TMH_default_refund_delay; 135 if (no_transfer_delay) 136 is.default_wire_transfer_delay = TMH_default_wire_transfer_delay; 137 if (no_rounding_interval) 138 is.default_wire_transfer_rounding_interval 139 = TMH_default_wire_transfer_rounding_interval; 140 if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) 141 { 142 GNUNET_break_op (0); 143 GNUNET_JSON_parse_free (spec); 144 return TALER_MHD_reply_with_error (connection, 145 MHD_HTTP_BAD_REQUEST, 146 TALER_EC_GENERIC_PARAMETER_MALFORMED, 147 "default_pay_delay"); 148 } 149 if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) 150 { 151 GNUNET_break_op (0); 152 GNUNET_JSON_parse_free (spec); 153 return TALER_MHD_reply_with_error (connection, 154 MHD_HTTP_BAD_REQUEST, 155 TALER_EC_GENERIC_PARAMETER_MALFORMED, 156 "default_refund_delay"); 157 } 158 if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) 159 { 160 GNUNET_break_op (0); 161 GNUNET_JSON_parse_free (spec); 162 return TALER_MHD_reply_with_error (connection, 163 MHD_HTTP_BAD_REQUEST, 164 TALER_EC_GENERIC_PARAMETER_MALFORMED, 165 "default_wire_transfer_delay"); 166 } 167 168 { 169 enum GNUNET_GenericReturnValue ret; 170 171 ret = TMH_check_auth_config (connection, 172 jauth, 173 &auth_password); 174 if (GNUNET_OK != ret) 175 { 176 GNUNET_JSON_parse_free (spec); 177 return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; 178 } 179 } 180 181 /* check 'id' well-formed */ 182 { 183 static bool once; 184 static regex_t reg; 185 bool id_wellformed = true; 186 187 if (! once) 188 { 189 once = true; 190 GNUNET_assert (0 == 191 regcomp (®, 192 "^[A-Za-z0-9][A-Za-z0-9_.@-]+$", 193 REG_EXTENDED)); 194 } 195 196 if (0 != regexec (®, 197 is.id, 198 0, NULL, 0)) 199 id_wellformed = false; 200 if (! id_wellformed) 201 { 202 GNUNET_JSON_parse_free (spec); 203 return TALER_MHD_reply_with_error (connection, 204 MHD_HTTP_BAD_REQUEST, 205 TALER_EC_GENERIC_PARAMETER_MALFORMED, 206 "id"); 207 } 208 } 209 210 if (! TMH_location_object_valid (is.address)) 211 { 212 GNUNET_break_op (0); 213 GNUNET_JSON_parse_free (spec); 214 return TALER_MHD_reply_with_error (connection, 215 MHD_HTTP_BAD_REQUEST, 216 TALER_EC_GENERIC_PARAMETER_MALFORMED, 217 "address"); 218 } 219 220 if (! TMH_location_object_valid (is.jurisdiction)) 221 { 222 GNUNET_break_op (0); 223 GNUNET_JSON_parse_free (spec); 224 return TALER_MHD_reply_with_error (connection, 225 MHD_HTTP_BAD_REQUEST, 226 TALER_EC_GENERIC_PARAMETER_MALFORMED, 227 "jurisdiction"); 228 } 229 230 if ( (NULL != is.logo) && 231 (! TMH_image_data_url_valid (is.logo)) ) 232 { 233 GNUNET_break_op (0); 234 GNUNET_JSON_parse_free (spec); 235 return TALER_MHD_reply_with_error (connection, 236 MHD_HTTP_BAD_REQUEST, 237 TALER_EC_GENERIC_PARAMETER_MALFORMED, 238 "logo"); 239 } 240 241 { 242 /* Test if an instance of this id is known */ 243 struct TMH_MerchantInstance *mi; 244 245 mi = TMH_lookup_instance (is.id); 246 if (NULL != mi) 247 { 248 if (mi->deleted) 249 { 250 GNUNET_JSON_parse_free (spec); 251 return TALER_MHD_reply_with_error (connection, 252 MHD_HTTP_CONFLICT, 253 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, 254 is.id); 255 } 256 /* Check for idempotency */ 257 if ( (0 == strcmp (mi->settings.id, 258 is.id)) && 259 (0 == strcmp (mi->settings.name, 260 is.name)) && 261 ((mi->settings.email == is.email) || 262 (NULL != is.email && NULL != mi->settings.email && 263 0 == strcmp (mi->settings.email, 264 is.email))) && 265 ((mi->settings.website == is.website) || 266 (NULL != is.website && NULL != mi->settings.website && 267 0 == strcmp (mi->settings.website, 268 is.website))) && 269 ((mi->settings.logo == is.logo) || 270 (NULL != is.logo && NULL != mi->settings.logo && 271 0 == strcmp (mi->settings.logo, 272 is.logo))) && 273 ( ( (NULL != auth_password) && 274 (GNUNET_OK == 275 TMH_check_auth (auth_password, 276 &mi->auth.auth_salt, 277 &mi->auth.auth_hash)) ) || 278 ( (NULL == auth_password) && 279 (GNUNET_YES == 280 GNUNET_is_zero (&mi->auth.auth_hash))) ) && 281 (1 == json_equal (mi->settings.address, 282 is.address)) && 283 (1 == json_equal (mi->settings.jurisdiction, 284 is.jurisdiction)) && 285 (mi->settings.use_stefan == is.use_stefan) && 286 (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, 287 ==, 288 is.default_wire_transfer_delay)) && 289 (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, 290 ==, 291 is.default_pay_delay)) && 292 (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, 293 ==, 294 is.default_refund_delay)) ) 295 { 296 GNUNET_JSON_parse_free (spec); 297 return TALER_MHD_reply_static (connection, 298 MHD_HTTP_NO_CONTENT, 299 NULL, 300 NULL, 301 0); 302 } 303 GNUNET_JSON_parse_free (spec); 304 return TALER_MHD_reply_with_error (connection, 305 MHD_HTTP_CONFLICT, 306 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, 307 is.id); 308 } 309 } 310 311 /* Check MFA is satisfied */ 312 if (validation_needed) 313 { 314 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; 315 316 if ( (0 != (TEH_TCS_SMS & TEH_mandatory_tan_channels)) && 317 (NULL == is.phone) ) 318 { 319 GNUNET_break_op (0); 320 GNUNET_JSON_parse_free (spec); 321 return TALER_MHD_reply_with_error (connection, 322 MHD_HTTP_BAD_REQUEST, 323 TALER_EC_GENERIC_PARAMETER_MISSING, 324 "phone_number"); 325 326 } 327 if ( (0 != (TEH_TCS_EMAIL & TEH_mandatory_tan_channels)) && 328 (NULL == is.email) ) 329 { 330 GNUNET_break_op (0); 331 GNUNET_JSON_parse_free (spec); 332 return TALER_MHD_reply_with_error (connection, 333 MHD_HTTP_BAD_REQUEST, 334 TALER_EC_GENERIC_PARAMETER_MISSING, 335 "email"); 336 } 337 switch (TEH_mandatory_tan_channels) 338 { 339 case TEH_TCS_NONE: 340 GNUNET_assert (0); 341 ret = GNUNET_OK; 342 break; 343 case TEH_TCS_SMS: 344 is.phone_validated = true; 345 ret = TMH_mfa_challenges_do (hc, 346 TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, 347 true, 348 TALER_MERCHANT_MFA_CHANNEL_SMS, 349 is.phone, 350 TALER_MERCHANT_MFA_CHANNEL_NONE); 351 break; 352 case TEH_TCS_EMAIL: 353 is.email_validated = true; 354 ret = TMH_mfa_challenges_do (hc, 355 TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, 356 true, 357 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 358 is.email, 359 TALER_MERCHANT_MFA_CHANNEL_NONE); 360 break; 361 case TEH_TCS_EMAIL_AND_SMS: 362 is.phone_validated = true; 363 is.email_validated = true; 364 ret = TMH_mfa_challenges_do (hc, 365 TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, 366 true, 367 TALER_MERCHANT_MFA_CHANNEL_SMS, 368 is.phone, 369 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 370 is.email, 371 TALER_MERCHANT_MFA_CHANNEL_NONE); 372 break; 373 } 374 if (GNUNET_OK != ret) 375 { 376 GNUNET_JSON_parse_free (spec); 377 return (GNUNET_NO == ret) 378 ? MHD_YES 379 : MHD_NO; 380 } 381 } 382 383 /* handle authentication token setup */ 384 if (NULL == auth_password) 385 { 386 memset (&ias.auth_salt, 387 0, 388 sizeof (ias.auth_salt)); 389 memset (&ias.auth_hash, 390 0, 391 sizeof (ias.auth_hash)); 392 } 393 else 394 { 395 /* Sets 'auth_salt' and 'auth_hash' */ 396 TMH_compute_auth (auth_password, 397 &ias.auth_salt, 398 &ias.auth_hash); 399 } 400 401 /* create in-memory data structure */ 402 { 403 struct TMH_MerchantInstance *mi; 404 enum GNUNET_DB_QueryStatus qs; 405 406 mi = GNUNET_new (struct TMH_MerchantInstance); 407 mi->wm_head = wm_head; 408 mi->wm_tail = wm_tail; 409 mi->settings = is; 410 mi->settings.address = json_incref (mi->settings.address); 411 mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); 412 mi->settings.id = GNUNET_strdup (is.id); 413 mi->settings.name = GNUNET_strdup (is.name); 414 if (NULL != is.email) 415 mi->settings.email = GNUNET_strdup (is.email); 416 if (NULL != is.phone) 417 mi->settings.phone = GNUNET_strdup (is.phone); 418 if (NULL != is.website) 419 mi->settings.website = GNUNET_strdup (is.website); 420 if (NULL != is.logo) 421 mi->settings.logo = GNUNET_strdup (is.logo); 422 mi->auth = ias; 423 mi->validation_needed = validation_needed; 424 GNUNET_CRYPTO_eddsa_key_create (&mi->merchant_priv.eddsa_priv); 425 GNUNET_CRYPTO_eddsa_key_get_public (&mi->merchant_priv.eddsa_priv, 426 &mi->merchant_pub.eddsa_pub); 427 428 for (unsigned int i = 0; i<MAX_RETRIES; i++) 429 { 430 if (GNUNET_OK != 431 TMH_db->start (TMH_db->cls, 432 "post /instances")) 433 { 434 mi->rc = 1; 435 TMH_instance_decref (mi); 436 GNUNET_JSON_parse_free (spec); 437 return TALER_MHD_reply_with_error (connection, 438 MHD_HTTP_INTERNAL_SERVER_ERROR, 439 TALER_EC_GENERIC_DB_START_FAILED, 440 NULL); 441 } 442 qs = TMH_db->insert_instance (TMH_db->cls, 443 &mi->merchant_pub, 444 &mi->merchant_priv, 445 &mi->settings, 446 &mi->auth, 447 validation_needed); 448 switch (qs) 449 { 450 case GNUNET_DB_STATUS_HARD_ERROR: 451 { 452 MHD_RESULT ret; 453 454 TMH_db->rollback (TMH_db->cls); 455 GNUNET_break (0); 456 ret = TALER_MHD_reply_with_error (connection, 457 MHD_HTTP_INTERNAL_SERVER_ERROR, 458 TALER_EC_GENERIC_DB_STORE_FAILED, 459 is.id); 460 mi->rc = 1; 461 TMH_instance_decref (mi); 462 GNUNET_JSON_parse_free (spec); 463 return ret; 464 } 465 case GNUNET_DB_STATUS_SOFT_ERROR: 466 goto retry; 467 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 468 { 469 MHD_RESULT ret; 470 471 TMH_db->rollback (TMH_db->cls); 472 GNUNET_break (0); 473 ret = TALER_MHD_reply_with_error (connection, 474 MHD_HTTP_CONFLICT, 475 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, 476 is.id); 477 mi->rc = 1; 478 TMH_instance_decref (mi); 479 GNUNET_JSON_parse_free (spec); 480 return ret; 481 } 482 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 483 /* handled below */ 484 break; 485 } 486 qs = TMH_db->commit (TMH_db->cls); 487 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 488 qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; 489 retry: 490 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 491 break; /* success! -- or hard failure */ 492 } /* for .. MAX_RETRIES */ 493 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 494 { 495 mi->rc = 1; 496 TMH_instance_decref (mi); 497 GNUNET_JSON_parse_free (spec); 498 return TALER_MHD_reply_with_error (connection, 499 MHD_HTTP_INTERNAL_SERVER_ERROR, 500 TALER_EC_GENERIC_DB_COMMIT_FAILED, 501 NULL); 502 } 503 /* Finally, also update our running process */ 504 GNUNET_assert (GNUNET_OK == 505 TMH_add_instance (mi)); 506 TMH_reload_instances (mi->settings.id); 507 } 508 GNUNET_JSON_parse_free (spec); 509 if (GNUNET_TIME_relative_is_zero (login_token_expiration)) 510 { 511 return TALER_MHD_reply_static (connection, 512 MHD_HTTP_NO_CONTENT, 513 NULL, 514 NULL, 515 0); 516 } 517 518 { 519 struct TALER_MERCHANTDB_LoginTokenP btoken; 520 enum TMH_AuthScope iscope = TMH_AS_REFRESHABLE | TMH_AS_SPA; 521 enum GNUNET_DB_QueryStatus qs; 522 struct GNUNET_TIME_Timestamp expiration_time; 523 bool refreshable = true; 524 525 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, 526 &btoken, 527 sizeof (btoken)); 528 expiration_time 529 = GNUNET_TIME_relative_to_timestamp (login_token_expiration); 530 qs = TMH_db->insert_login_token (TMH_db->cls, 531 is.id, 532 &btoken, 533 GNUNET_TIME_timestamp_get (), 534 expiration_time, 535 iscope, 536 "login token from instance creation"); 537 switch (qs) 538 { 539 case GNUNET_DB_STATUS_HARD_ERROR: 540 case GNUNET_DB_STATUS_SOFT_ERROR: 541 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 542 GNUNET_break (0); 543 return TALER_MHD_reply_with_ec (connection, 544 TALER_EC_GENERIC_DB_STORE_FAILED, 545 "insert_login_token"); 546 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 547 break; 548 } 549 550 { 551 char *tok; 552 MHD_RESULT ret; 553 char *val; 554 555 val = GNUNET_STRINGS_data_to_string_alloc (&btoken, 556 sizeof (btoken)); 557 GNUNET_asprintf (&tok, 558 RFC_8959_PREFIX "%s", 559 val); 560 GNUNET_free (val); 561 ret = TALER_MHD_REPLY_JSON_PACK ( 562 connection, 563 MHD_HTTP_OK, 564 GNUNET_JSON_pack_string ("access_token", 565 tok), 566 GNUNET_JSON_pack_string ("token", 567 tok), 568 GNUNET_JSON_pack_string ("scope", 569 TMH_get_name_by_scope (iscope, 570 &refreshable)), 571 GNUNET_JSON_pack_bool ("refreshable", 572 refreshable), 573 GNUNET_JSON_pack_timestamp ("expiration", 574 expiration_time)); 575 GNUNET_free (tok); 576 return ret; 577 } 578 } 579 } 580 581 582 /** 583 * Generate an instance, given its configuration. 584 * 585 * @param rh context of the handler 586 * @param connection the MHD connection to handle 587 * @param[in,out] hc context with further information about the request 588 * @return MHD result code 589 */ 590 MHD_RESULT 591 TMH_private_post_instances (const struct TMH_RequestHandler *rh, 592 struct MHD_Connection *connection, 593 struct TMH_HandlerContext *hc) 594 { 595 return post_instances (rh, 596 connection, 597 hc, 598 GNUNET_TIME_UNIT_ZERO, 599 false); 600 } 601 602 603 /** 604 * Generate an instance, given its configuration. 605 * Public handler to be used when self-provisioning. 606 * 607 * @param rh context of the handler 608 * @param connection the MHD connection to handle 609 * @param[in,out] hc context with further information about the request 610 * @return MHD result code 611 */ 612 MHD_RESULT 613 TMH_public_post_instances (const struct TMH_RequestHandler *rh, 614 struct MHD_Connection *connection, 615 struct TMH_HandlerContext *hc) 616 { 617 struct GNUNET_TIME_Relative expiration; 618 619 TALER_MHD_parse_request_rel_time (connection, 620 "token_validity_ms", 621 &expiration); 622 if (GNUNET_YES != 623 TMH_have_self_provisioning) 624 { 625 GNUNET_break_op (0); 626 return TALER_MHD_reply_with_error (connection, 627 MHD_HTTP_FORBIDDEN, 628 TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, 629 "Self-provisioning is not enabled"); 630 } 631 632 return post_instances (rh, 633 connection, 634 hc, 635 expiration, 636 TEH_TCS_NONE != 637 TEH_mandatory_tan_channels); 638 } 639 640 641 /* end of taler-merchant-httpd_private-post-instances.c */