/* This file is part of TALER (C) 2014, 2015, 2016, 2018 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 backend/taler-merchant-httpd_proposal.c * @brief HTTP serving layer mainly intended to communicate with the frontend * @author Marcello Stanisci */ #include "platform.h" #include #include #include #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_parsing.h" #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_responses.h" /** * How often do we retry the simple INSERT database transaction? */ #define MAX_RETRIES 3 /** * Check that the given JSON array of products is well-formed. * * @param products JSON array to check * @return #GNUNET_OK if all is fine */ static int check_products (json_t *products) { size_t index; json_t *value; int res; if (! json_is_array (products)) { GNUNET_break (0); return GNUNET_SYSERR; } json_array_foreach (products, index, value) { const char *description; const char *error_name; unsigned int error_line; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("description", &description), /* FIXME: there are other fields in the product specification that are currently not labeled as optional. Maybe check those as well, or make them truly optional. */ GNUNET_JSON_spec_end() }; /* extract fields we need to sign separately */ res = GNUNET_JSON_parse (value, spec, &error_name, &error_line); if (GNUNET_OK != res) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Product description parsing failed at #%u: %s:%u\n", (unsigned int) index, error_name, error_line); return GNUNET_SYSERR; } GNUNET_JSON_parse_free (spec); } return GNUNET_OK; } /** * Information we keep for individual calls * to requests that parse JSON, but keep no other state. */ struct TMH_JsonParseContext { /** * This field MUST be first. * FIXME: Explain why! */ struct TM_HandlerContext hc; /** * Placeholder for #TMH_PARSE_post_json() to keep its internal state. */ void *json_parse_context; }; /** * Custom cleanup routine for a `struct TMH_JsonParseContext`. * * @param hc the `struct TMH_JsonParseContext` to clean up. */ static void json_parse_cleanup (struct TM_HandlerContext *hc) { struct TMH_JsonParseContext *jpc = (struct TMH_JsonParseContext *) hc; TMH_PARSE_post_cleanup_callback (jpc->json_parse_context); GNUNET_free (jpc); } /** * Transform an order into a proposal and store it in the database. * Write the resulting proposal or an error message ot a MHD connection * * @param connection connection to write the result or error to * @param order to process * @return MHD result code */ static int proposal_put (struct MHD_Connection *connection, json_t *order) { int res; struct MerchantInstance *mi; struct TALER_Amount total; const char *order_id; const char *summary; const char *fulfillment_url; json_t *products; json_t *merchant; struct GNUNET_TIME_Absolute timestamp; struct GNUNET_TIME_Absolute refund_deadline; struct GNUNET_TIME_Absolute pay_deadline; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("amount", &total), GNUNET_JSON_spec_string ("order_id", &order_id), GNUNET_JSON_spec_string ("summary", &summary), GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url), /** * The following entries we don't actually need, * except to check that the order is well-formed */ GNUNET_JSON_spec_json ("products", &products), GNUNET_JSON_spec_json ("merchant", &merchant), GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp), GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), GNUNET_JSON_spec_absolute_time ("pay_deadline", &pay_deadline), GNUNET_JSON_spec_end () }; enum GNUNET_DB_QueryStatus qs; const char *instance; struct WireMethod *wm; /* Add order_id if it doesn't exist. */ if (NULL == json_string_value (json_object_get (order, "order_id"))) { char buf[256]; time_t timer; struct tm* tm_info; size_t off; time (&timer); tm_info = localtime (&timer); if (NULL == tm_info) { return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_NO_LOCALTIME, "failed to determine local time"); } off = strftime (buf, sizeof (buf), "%Y.%j.%H.%M.%S", tm_info); buf[off++] = '-'; uint64_t rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, UINT64_MAX); char *last = GNUNET_STRINGS_data_to_string (&rand, sizeof (uint64_t), &buf[off], sizeof (buf) - off); *last = '\0'; json_object_set_new (order, "order_id", json_string (buf)); } /* Add timestamp if it doesn't exist */ if (NULL == json_object_get (order, "timestamp")) { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); (void) GNUNET_TIME_round_abs (&now); json_object_set_new (order, "timestamp", GNUNET_JSON_from_time_abs (now)); } if (NULL == json_object_get (order, "refund_deadline")) { struct GNUNET_TIME_Absolute zero = { 0 }; json_object_set_new (order, "refund_deadline", GNUNET_JSON_from_time_abs (zero)); } if (NULL == json_object_get (order, "pay_deadline")) { struct GNUNET_TIME_Absolute t; t = GNUNET_TIME_relative_to_absolute (default_pay_deadline); (void) GNUNET_TIME_round_abs (&t); json_object_set_new (order, "pay_deadline", GNUNET_JSON_from_time_abs (t)); } if (NULL == json_object_get (order, "max_wire_fee")) { json_object_set_new (order, "max_wire_fee", TALER_JSON_from_amount (&default_max_wire_fee)); } if (NULL == json_object_get (order, "max_fee")) { json_object_set_new (order, "max_fee", TALER_JSON_from_amount (&default_max_deposit_fee)); } if (NULL == json_object_get (order, "wire_fee_amortization")) { json_object_set_new (order, "wire_fee_amortization", json_integer ((json_int_t) default_wire_fee_amortization)); } if (NULL == json_object_get (order, "pay_url")) { char *url; url = TALER_url_absolute_mhd (connection, "/public/pay", NULL); json_object_set_new (order, "pay_url", json_string (url)); GNUNET_free (url); } if (NULL == json_object_get (order, "products")) { // FIXME: When there is no explicit product, // should we create a singleton product list? json_object_set_new (order, "products", json_array ()); } instance = json_string_value (json_object_get (order, "instance")); if (NULL == instance) { instance = "default"; } // Fill in merchant information if necessary { // The frontend either fully specifieds the "merchant" field, or just gives // the backend the "instance" name and lets it fill out. struct MerchantInstance *mi = TMH_lookup_instance (instance); json_t *merchant; json_t *locations; json_t *loc; char *label; if (NULL == mi) { return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, "merchant instance not found"); } if (NULL == json_object_get (order, "merchant")) { merchant = json_object (); /* FIXME: should the 'instance' field really be included in the contract? This is really internal to the business! */ json_object_set_new (merchant, "instance", json_string (instance)); json_object_set_new (merchant, "name", json_string (mi->name)); json_object_set_new (merchant, "jurisdiction", json_string ("_mj")); json_object_set_new (merchant, "address", json_string ("_ma")); json_object_set_new (order, "merchant", merchant); json_object_del (order, "instance"); locations = json_object_get (order, "locations"); if (NULL == locations) { locations = json_object (); json_object_set_new (order, "locations", locations); } GNUNET_assert (0 < GNUNET_asprintf (&label, "%s-address", mi->id)); loc = json_object_get (default_locations, label); if (NULL == loc) loc = json_object (); else loc = json_deep_copy (loc); json_object_set_new (locations, "_ma", loc); GNUNET_free (label); GNUNET_assert (0 < GNUNET_asprintf (&label, "%s-jurisdiction", mi->id)); loc = json_object_get (default_locations, label); if (NULL == loc) loc = json_object (); else loc = json_deep_copy (loc); json_object_set_new (locations, "_mj", loc); GNUNET_free (label); } } /* extract fields we need to sign separately */ res = TMH_PARSE_json_data (connection, order, spec); if (GNUNET_NO == res) { return MHD_YES; } if (GNUNET_SYSERR == res) { return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, "Impossible to parse the order"); } /* check contract is well-formed */ if (GNUNET_OK != check_products (products)) { GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_arg_invalid (connection, TALER_EC_PARAMETER_MALFORMED, "order:products"); } mi = TMH_lookup_instance_json (merchant); if (NULL == mi) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Not able to find the specified instance\n"); GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_not_found (connection, TALER_EC_CONTRACT_INSTANCE_UNKNOWN, "Unknown instance given"); } /* add fields to the contract that the backend should provide */ json_object_set (order, "exchanges", trusted_exchanges); json_object_set (order, "auditors", j_auditors); /* TODO (#4939-12806): add proper mechanism for selection of wire method(s) by merchant! */ wm = mi->wm_head; if (NULL == wm) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No wire method available for specified instance\n"); GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_not_found (connection, TALER_EC_CONTRACT_INSTANCE_UNKNOWN, "No wire method configured for instance"); } json_object_set_new (order, "H_wire", GNUNET_JSON_from_data_auto (&wm->h_wire)); json_object_set_new (order, "wire_method", json_string (wm->wire_method)); json_object_set_new (order, "merchant_pub", GNUNET_JSON_from_data_auto (&mi->pubkey)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Inserting order '%s' for instance '%s'\n", order_id, mi->id); { json_t *dummy_contract_terms; dummy_contract_terms = NULL; qs = db->find_order (db->cls, &dummy_contract_terms, order_id, &mi->pubkey); if (NULL != dummy_contract_terms) json_decref (dummy_contract_terms); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) { if ( (GNUNET_DB_STATUS_SOFT_ERROR == qs) || (GNUNET_DB_STATUS_HARD_ERROR == qs) ) { return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_STORE_DB_ERROR, "db error: could not check for existing order"); } return TMH_RESPONSE_reply_external_error (connection, TALER_EC_PROPOSAL_STORE_DB_ERROR, "proposal already exists"); } for (unsigned int i=0;iinsert_order (db->cls, order_id, &mi->pubkey, timestamp, order); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } if (0 > qs) { /* Special report if retries insufficient */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_STORE_DB_ERROR, "db error: could not store this proposal's data into db"); } res = TMH_RESPONSE_reply_json_pack (connection, MHD_HTTP_OK, "{s:s}", "order_id", order_id); GNUNET_JSON_parse_free (spec); return res; } /** * Generate a proposal, given its order. In practical terms, it adds the * fields 'exchanges', 'merchant_pub', and 'H_wire' to the order gotten * from the frontend. Finally, it signs this data, and returns it to the * frontend. * * @param connection the MHD connection to handle * @param[in,out] connection_cls the connection's closure (can be updated) * @param upload_data upload data * @param[in,out] upload_data_size number of bytes (left) in @a upload_data * @return MHD result code */ int MH_handler_proposal_put (struct TMH_RequestHandler *rh, struct MHD_Connection *connection, void **connection_cls, const char *upload_data, size_t *upload_data_size) { int res; struct TMH_JsonParseContext *ctx; json_t *root; json_t *order; if (NULL == *connection_cls) { ctx = GNUNET_new (struct TMH_JsonParseContext); ctx->hc.cc = &json_parse_cleanup; *connection_cls = ctx; } else { ctx = *connection_cls; } res = TMH_PARSE_post_json (connection, &ctx->json_parse_context, upload_data, upload_data_size, &root); if (GNUNET_SYSERR == res) return MHD_NO; /* the POST's body has to be further fetched */ if ( (GNUNET_NO == res) || (NULL == root) ) return MHD_YES; order = json_object_get (root, "order"); if (NULL == order) { res = TMH_RESPONSE_reply_arg_missing (connection, TALER_EC_PARAMETER_MISSING, "order"); } else { res = proposal_put (connection, order); } json_decref (root); return res; } /** * Manage a GET /proposal request. Query the db and returns the * proposal's data related to the transaction id given as the URL's * parameter. * * Binds the proposal to a nonce. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] connection_cls the connection's closure (can be updated) * @param upload_data upload data * @param[in,out] upload_data_size number of bytes (left) in @a upload_data * @return MHD result code */ int MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, struct MHD_Connection *connection, void **connection_cls, const char *upload_data, size_t *upload_data_size) { const char *order_id; const char *instance; const char *nonce; int res; enum GNUNET_DB_QueryStatus qs; json_t *contract_terms; struct MerchantInstance *mi; char *last_session_id = NULL; instance = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "instance"); if (NULL == instance) return TMH_RESPONSE_reply_arg_missing (connection, TALER_EC_PARAMETER_MISSING, "instance"); mi = TMH_lookup_instance (instance); if (NULL == mi) return TMH_RESPONSE_reply_not_found (connection, TALER_EC_CONTRACT_INSTANCE_UNKNOWN, "instance"); order_id = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "order_id"); if (NULL == order_id) return TMH_RESPONSE_reply_arg_missing (connection, TALER_EC_PARAMETER_MISSING, "order_id"); nonce = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "nonce"); if (NULL == nonce) return TMH_RESPONSE_reply_arg_missing (connection, TALER_EC_PARAMETER_MISSING, "nonce"); qs = db->find_contract_terms (db->cls, &contract_terms, &last_session_id, order_id, &mi->pubkey); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_LOOKUP_DB_ERROR, "An error occurred while retrieving proposal data from db"); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { qs = db->find_order (db->cls, &contract_terms, order_id, &mi->pubkey); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { return TMH_RESPONSE_reply_not_found (connection, TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, "unknown order id"); } GNUNET_assert (NULL != contract_terms); // FIXME: now we can delete (merchant_pub, order_id) from the merchant_orders table json_object_set_new (contract_terms, "nonce", json_string (nonce)); struct GNUNET_TIME_Absolute timestamp; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp), GNUNET_JSON_spec_end () }; /* extract fields we need to sign separately */ res = TMH_PARSE_json_data (connection, contract_terms, spec); if (GNUNET_NO == res) { return MHD_YES; } if (GNUNET_SYSERR == res) { return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, "Impossible to parse the order"); } for (unsigned int i=0;iinsert_contract_terms (db->cls, order_id, &mi->pubkey, timestamp, contract_terms); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } if (0 > qs) { /* Special report if retries insufficient */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_STORE_DB_ERROR, "db error: could not store this proposal's data into db"); } } GNUNET_assert (NULL != contract_terms); GNUNET_free_non_null (last_session_id); const char *stored_nonce = json_string_value (json_object_get (contract_terms, "nonce")); if (NULL == stored_nonce) { GNUNET_break (0); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PROPOSAL_ORDER_PARSE_ERROR, "existing proposal has non nonce"); } if (0 != strcmp (stored_nonce, nonce)) { return TMH_RESPONSE_reply_bad_request (connection, TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, "mismatched nonce"); } struct TALER_ProposalDataPS pdps; struct GNUNET_CRYPTO_EddsaSignature merchant_sig; /* create proposal signature */ pdps.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT); pdps.purpose.size = htonl (sizeof (pdps)); if (GNUNET_OK != TALER_JSON_hash (contract_terms, &pdps.hash)) { GNUNET_break (0); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_INTERNAL_LOGIC_ERROR, "Could not hash order"); } GNUNET_CRYPTO_eddsa_sign (&mi->privkey.eddsa_priv, &pdps.purpose, &merchant_sig); res = TMH_RESPONSE_reply_json_pack (connection, MHD_HTTP_OK, "{ s:o, s:o }", "contract_terms", contract_terms, "sig", GNUNET_JSON_from_data_auto (&merchant_sig)); return res; } /* end of taler-merchant-httpd_proposal.c */