/* This file is part of TALER (C) 2020, 2021 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_private-post-instances.c * @brief implementing POST /instances request handling * @author Christian Grothoff */ #include "platform.h" #include "taler-merchant-httpd_private-post-instances.h" #include "taler-merchant-httpd_helper.h" #include #include /** * How often do we retry the simple INSERT database transaction? */ #define MAX_RETRIES 3 /** * Check if the array of @a payto_uris contains exactly the same * URIs as those already in @a mi (possibly in a different order). * * @param mi a merchant instance with accounts * @param payto_uris a JSON array with accounts (presumably) * @return true if they are 'equal', false if not or of payto_uris is not an array */ static bool accounts_equal (const struct TMH_MerchantInstance *mi, json_t *payto_uris) { if (! json_is_array (payto_uris)) return false; { unsigned int len = json_array_size (payto_uris); bool matches[GNUNET_NZL (len)]; struct TMH_WireMethod *wm; memset (matches, 0, sizeof (matches)); for (wm = mi->wm_head; NULL != wm; wm = wm->next) { const char *uri = json_string_value (json_object_get (wm->j_wire, "payto_uri")); GNUNET_assert (NULL != uri); for (unsigned int i = 0; ij_wire); GNUNET_free (wm->wire_method); GNUNET_free (wm); } /** * Free memory used by @a mi. * * @param mi instance to free */ static void free_mi (struct TMH_MerchantInstance *mi) { struct TMH_WireMethod *wm; while (NULL != (wm = mi->wm_head)) { GNUNET_CONTAINER_DLL_remove (mi->wm_head, mi->wm_tail, wm); free_wm (wm); } GNUNET_free (mi->settings.id); GNUNET_free (mi->settings.name); json_decref (mi->settings.address); json_decref (mi->settings.jurisdiction); GNUNET_free (mi); } /** * Generate an instance, given its configuration. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT TMH_private_post_instances (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct TALER_MERCHANTDB_InstanceSettings is; struct TALER_MERCHANTDB_InstanceAuthSettings ias; json_t *payto_uris; const char *auth_token = NULL; struct TMH_WireMethod *wm_head = NULL; struct TMH_WireMethod *wm_tail = NULL; json_t *jauth; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("payto_uris", &payto_uris), GNUNET_JSON_spec_string ("id", (const char **) &is.id), GNUNET_JSON_spec_string ("name", (const char **) &is.name), GNUNET_JSON_spec_json ("auth", &jauth), GNUNET_JSON_spec_json ("address", &is.address), GNUNET_JSON_spec_json ("jurisdiction", &is.jurisdiction), TALER_JSON_spec_amount ("default_max_wire_fee", TMH_currency, &is.default_max_wire_fee), GNUNET_JSON_spec_uint32 ("default_wire_fee_amortization", &is.default_wire_fee_amortization), TALER_JSON_spec_amount ("default_max_deposit_fee", TMH_currency, &is.default_max_deposit_fee), TALER_JSON_spec_relative_time ("default_wire_transfer_delay", &is.default_wire_transfer_delay), TALER_JSON_spec_relative_time ("default_pay_delay", &is.default_pay_delay), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } { enum GNUNET_GenericReturnValue ret; ret = TMH_check_auth_config (connection, jauth, &auth_token); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } /* check payto_uris for well-formedness */ if (! TMH_payto_uri_array_valid (payto_uris)) return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PAYTO_URI_MALFORMED, NULL); /* check 'id' well-formed */ { static bool once; static regex_t reg; bool id_wellformed = true; if (! once) { GNUNET_assert (0 == regcomp (®, "^[A-Za-z0-9][A-Za-z0-9_.@-]+$", REG_EXTENDED)); } if (0 != regexec (®, is.id, 0, NULL, 0)) id_wellformed = false; if (! id_wellformed) return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "id"); } if (! TMH_location_object_valid (is.address)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "address"); } if (! TMH_location_object_valid (is.jurisdiction)) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "jurisdiction"); } { /* Test if an instance of this id is known */ struct TMH_MerchantInstance *mi; mi = TMH_lookup_instance (is.id); if (NULL != mi) { if (mi->deleted) { GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, is.id); } /* Check for idempotency */ if ( (0 == strcmp (mi->settings.id, is.id)) && (0 == strcmp (mi->settings.name, is.name)) && ( ( (NULL != auth_token) && (GNUNET_OK == TMH_check_auth (auth_token, &mi->auth.auth_salt, &mi->auth.auth_hash)) ) || ( (NULL == auth_token) && (GNUNET_YES == GNUNET_is_zero (&mi->auth.auth_hash))) ) && (1 == json_equal (mi->settings.address, is.address)) && (1 == json_equal (mi->settings.jurisdiction, is.jurisdiction)) && (GNUNET_OK == TALER_amount_cmp_currency ( &mi->settings.default_max_deposit_fee, &is.default_max_deposit_fee)) && (0 == TALER_amount_cmp (&mi->settings.default_max_deposit_fee, &is.default_max_deposit_fee)) && (GNUNET_OK == TALER_amount_cmp_currency ( &mi->settings.default_max_wire_fee, &is.default_max_wire_fee)) && (0 == TALER_amount_cmp (&mi->settings.default_max_wire_fee, &is.default_max_wire_fee)) && (mi->settings.default_wire_fee_amortization == is.default_wire_fee_amortization) && (mi->settings.default_wire_transfer_delay.rel_value_us == is.default_wire_transfer_delay.rel_value_us) && (mi->settings.default_pay_delay.rel_value_us == is.default_pay_delay.rel_value_us) && (accounts_equal (mi, payto_uris)) ) { GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); } else { GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, is.id); } } } /* convert provided payto URIs into internal data structure with salts */ { unsigned int len = json_array_size (payto_uris); for (unsigned int i = 0; iwm_head = wm_head; mi->wm_tail = wm_tail; mi->settings = is; mi->settings.address = json_incref (mi->settings.address); mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); mi->settings.id = GNUNET_strdup (is.id); mi->settings.name = GNUNET_strdup (is.name); mi->auth = ias; GNUNET_CRYPTO_eddsa_key_create (&mi->merchant_priv.eddsa_priv); GNUNET_CRYPTO_eddsa_key_get_public (&mi->merchant_priv.eddsa_priv, &mi->merchant_pub.eddsa_pub); for (unsigned int i = 0; istart (TMH_db->cls, "post /instances")) { GNUNET_JSON_parse_free (spec); free_mi (mi); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL); } qs = TMH_db->insert_instance (TMH_db->cls, &mi->merchant_pub, &mi->merchant_priv, &mi->settings, &mi->auth); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { MHD_RESULT ret; TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) goto retry; ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, is.id); GNUNET_JSON_parse_free (spec); free_mi (mi); return ret; } for (struct TMH_WireMethod *wm = wm_head; NULL != wm; wm = wm->next) { struct TALER_MERCHANTDB_AccountDetails ad; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("payto_uri", &ad.payto_uri), GNUNET_JSON_spec_fixed_auto ("salt", &ad.salt), GNUNET_JSON_spec_end () }; GNUNET_assert (GNUNET_OK == TALER_MHD_parse_json_data (NULL, wm->j_wire, spec)); ad.h_wire = wm->h_wire; ad.active = wm->active; qs = TMH_db->insert_account (TMH_db->cls, mi->settings.id, &ad); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) break; } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_HARD_ERROR == qs) break; goto retry; } qs = TMH_db->commit (TMH_db->cls); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; retry: if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; /* success! -- or hard failure */ } /* for .. MAX_RETRIES */ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { GNUNET_JSON_parse_free (spec); free_mi (mi); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); } /* Finally, also update our running process */ GNUNET_assert (GNUNET_OK == TMH_add_instance (mi)); TMH_reload_instances (mi->settings.id); } GNUNET_JSON_parse_free (spec); GNUNET_free (TMH_default_auth); /* clear it: user just either created default instance or it should already be NULL */ return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); } /* end of taler-merchant-httpd_private-post-instances.c */