/* This file is part of TALER (C) 2014, 2015, 2016, 2018, 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-orders.c * @brief the POST /orders handler * @author Christian Grothoff * @author Marcello Stanisci */ #include "platform.h" #include #include #include #include "taler-merchant-httpd_private-post-orders.h" #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_private-get-orders.h" /** * How often do we retry the simple INSERT database transaction? */ #define MAX_RETRIES 3 /** * What is the label under which we find/place the merchant's * jurisdiction in the locations list by default? */ #define STANDARD_LABEL_MERCHANT_JURISDICTION "_mj" /** * What is the label under which we find/place the merchant's * address in the locations list by default? */ #define STANDARD_LABEL_MERCHANT_ADDRESS "_ma" /** * 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 enum GNUNET_GenericReturnValue check_products (const json_t *products) { size_t index; json_t *value; 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; enum GNUNET_GenericReturnValue res; struct GNUNET_JSON_Specification spec[] = { // FIXME: parse and format-validate all // optional fields of a product and check validity GNUNET_JSON_spec_string ("description", &description), 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 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; } /** * Generate the base URL for the given merchant instance. * * @param connection the MHD connection * @param instance_id the merchant instance ID * @returns the merchant instance's base URL */ static char * make_merchant_base_url (struct MHD_Connection *connection, const char *instance_id) { const char *host; const char *forwarded_host; const char *uri_path; struct GNUNET_Buffer buf = { 0 }; if (GNUNET_YES == TALER_mhd_is_https (connection)) GNUNET_buffer_write_str (&buf, "https://"); else GNUNET_buffer_write_str (&buf, "http://"); host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_HOST); forwarded_host = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "X-Forwarded-Host"); if (NULL != forwarded_host) { GNUNET_buffer_write_str (&buf, forwarded_host); } else { GNUNET_assert (NULL != host); GNUNET_buffer_write_str (&buf, host); } uri_path = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, "X-Forwarded-Prefix"); if (NULL != uri_path) { /* Currently the merchant backend is only supported at the root of the path, this might change in the future. */ GNUNET_assert (0); } if (0 != strcmp (instance_id, "default")) { GNUNET_buffer_write_path (&buf, "/instances/"); GNUNET_buffer_write_str (&buf, instance_id); } GNUNET_buffer_write_path (&buf, ""); return GNUNET_buffer_reap_str (&buf); } /** * Information about a product we are supposed to add to the order * based on what we know it from our inventory. */ struct InventoryProduct { /** * Identifier of the product in the inventory. */ const char *product_id; /** * Number of units of the product to add to the order. */ uint32_t quantity; }; /** * Execute the database transaction to setup the order. * * @param hc handler context for the request * @param order_id unique ID for the order * @param h_post_data hash of the client's POST request, for idempotency checks * @param pay_deadline until when does the order have to be paid * @param[in] order order to process (not modified) * @param claim_token token to use for access control * @param inventory_products_length length of the @a inventory_products array * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products * @param[out] out_of_stock_index which product (by offset) is out of stock, UINT_MAX if all were in-stock * @return transaction status, 0 if @a uuids were insufficient to reserve required inventory */ static enum GNUNET_DB_QueryStatus execute_transaction (struct TMH_HandlerContext *hc, const char *order_id, const struct GNUNET_HashCode *h_post_data, struct GNUNET_TIME_Absolute pay_deadline, const json_t *order, const struct TALER_ClaimTokenP *claim_token, unsigned int inventory_products_length, const struct InventoryProduct inventory_products[], unsigned int uuids_length, const struct GNUNET_Uuid uuids[], unsigned int *out_of_stock_index) { enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Absolute timestamp; uint64_t order_serial; if (GNUNET_OK != TMH_db->start (TMH_db->cls, "insert_order")) { GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } /* Setup order */ qs = TMH_db->insert_order (TMH_db->cls, hc->instance->settings.id, order_id, h_post_data, pay_deadline, claim_token, order); if (qs <= 0) { /* qs == 0: probably instance does not exist (anymore) */ TMH_db->rollback (TMH_db->cls); return qs; } /* Migrate locks from UUIDs to new order: first release old locks */ for (unsigned int i = 0; iunlock_inventory (TMH_db->cls, &uuids[i]); if (qs < 0) { TMH_db->rollback (TMH_db->cls); return qs; } /* qs == 0 is OK here, that just means we did not HAVE any lock under this UUID */ } /* Migrate locks from UUIDs to new order: acquire new locks (note: this can basically ONLY fail on serializability OR because the UUID locks were insufficient for the desired quantities). */ for (unsigned int i = 0; iinsert_order_lock (TMH_db->cls, hc->instance->settings.id, order_id, inventory_products[i].product_id, inventory_products[i].quantity); if (qs < 0) { TMH_db->rollback (TMH_db->cls); return qs; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* qs == 0: lock acquisition failed due to insufficient stocks */ TMH_db->rollback (TMH_db->cls); *out_of_stock_index = i; /* indicate which product is causing the issue */ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } } *out_of_stock_index = UINT_MAX; /* Get the order serial and timestamp for the order we just created to update long-poll clients. */ qs = TMH_db->lookup_order_summary (TMH_db->cls, hc->instance->settings.id, order_id, ×tamp, &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { TMH_db->rollback (TMH_db->cls); return qs; } /* finally, commit transaction (note: if it fails, we ALSO re-acquire the UUID locks, which is exactly what we want) */ qs = TMH_db->commit (TMH_db->cls); if (0 > qs) return qs; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */ } /** * Transform an order into a proposal and store it in the * database. Write the resulting proposal or an error message * of a MHD connection. * * @param connection connection to write the result or error to * @param hc handler context for the request * @param h_post_data hash of the client's POST request, for idempotency checks * @param[in,out] order order to process (can be modified) * @param claim_token token to use for access control * @param inventory_products_length length of the @a inventory_products array * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products * @return MHD result code */ static MHD_RESULT execute_order (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, const struct GNUNET_HashCode *h_post_data, json_t *order, const struct TALER_ClaimTokenP *claim_token, unsigned int inventory_products_length, const struct InventoryProduct inventory_products[], unsigned int uuids_length, const struct GNUNET_Uuid uuids[]) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &hc->instance->settings; struct TALER_Amount total; const char *order_id; const char *summary; json_t *products; json_t *merchant; struct GNUNET_TIME_Absolute timestamp; struct GNUNET_TIME_Absolute refund_deadline; struct GNUNET_TIME_Absolute wire_transfer_deadline; struct GNUNET_TIME_Absolute pay_deadline; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("amount", TMH_currency, &total), GNUNET_JSON_spec_string ("order_id", &order_id), GNUNET_JSON_spec_string ("summary", &summary), /** * 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), TALER_JSON_spec_absolute_time ("timestamp", ×tamp), TALER_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), TALER_JSON_spec_absolute_time ("pay_deadline", &pay_deadline), TALER_JSON_spec_absolute_time ("wire_transfer_deadline", &wire_transfer_deadline), GNUNET_JSON_spec_end () }; enum GNUNET_DB_QueryStatus qs; unsigned int out_of_stock_index; /* extract fields we need to sign separately */ { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, order, spec); if (GNUNET_OK != res) { GNUNET_break_op (0); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } } /* check product list in contract is well-formed */ if (GNUNET_OK != check_products (products)) { GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "order:products"); } /* Test if we already have an order with this id */ { struct TALER_ClaimTokenP token; json_t *contract_terms; struct GNUNET_HashCode orig_post; TMH_db->preflight (TMH_db->cls); qs = TMH_db->lookup_order (TMH_db->cls, hc->instance->settings.id, order_id, &token, &orig_post, &contract_terms); /* If yes, check for idempotency */ if (0 > qs) { GNUNET_break (0); TMH_db->rollback (TMH_db->cls); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_order"); } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { MHD_RESULT ret; json_decref (contract_terms); /* Comparing the contract terms is sufficient because all the other params get added to it at some point. */ if (0 == GNUNET_memcmp (&orig_post, h_post_data)) { ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, GNUNET_JSON_pack_string ("order_id", order_id), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_varsize ( "token", GNUNET_is_zero (&token) ? NULL : &token, sizeof (token)))); } else { /* This request is not idempotent */ ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, order_id); } GNUNET_JSON_parse_free (spec); return ret; } } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Executing database transaction to create order '%s' for instance '%s'\n", order_id, settings->id); for (unsigned int i = 0; ipreflight (TMH_db->cls); qs = execute_transaction (hc, order_id, h_post_data, pay_deadline, order, claim_token, inventory_products_length, inventory_products, uuids_length, uuids, &out_of_stock_index); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } if (0 >= qs) { GNUNET_JSON_parse_free (spec); /* Special report if retries insufficient */ if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* should be: contract (!) with same order ID already exists */ return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, order_id); } /* Other hard transaction error (disk full, etc.) */ GNUNET_break (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); } /* DB transaction succeeded, check for out-of-stock */ if (out_of_stock_index < UINT_MAX) { /* We had a product that has insufficient quantities, generate the details for the response. */ struct TALER_MERCHANTDB_ProductDetails pd; MHD_RESULT ret; memset (&pd, 0, sizeof (pd)); qs = TMH_db->lookup_product ( TMH_db->cls, hc->instance->settings.id, inventory_products[out_of_stock_index].product_id, &pd); GNUNET_JSON_parse_free (spec); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_GONE, GNUNET_JSON_pack_string ( "product_id", inventory_products[out_of_stock_index].product_id), GNUNET_JSON_pack_uint64 ( "requested_quantity", inventory_products[out_of_stock_index].quantity), GNUNET_JSON_pack_uint64 ( "available_quantity", pd.total_stock - pd.total_sold - pd.total_lost), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_time_abs ( "restock_expected", pd.next_restock))); TALER_MERCHANTDB_product_details_free (&pd); return ret; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_GONE, GNUNET_JSON_pack_string ( "product_id", inventory_products[out_of_stock_index].product_id), GNUNET_JSON_pack_uint64 ( "requested_quantity", inventory_products[out_of_stock_index].quantity), GNUNET_JSON_pack_uint64 ( "available_quantity", 0)); case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); case GNUNET_DB_STATUS_HARD_ERROR: return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, NULL); } GNUNET_break (0); return MHD_NO; } /* Everything in-stock, generate positive response */ { MHD_RESULT ret; ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, GNUNET_JSON_pack_string ("order_id", order_id), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_varsize ( "token", GNUNET_is_zero (claim_token) ? NULL : claim_token, sizeof (*claim_token)))); GNUNET_JSON_parse_free (spec); return ret; } } /** * Add missing fields to the order. Upon success, continue * processing with execute_order(). * * @param connection connection to write the result or error to * @param hc handler context for the request * @param h_post_data hash of the client's POST request, for idempotency checks * @param[in,out] order order to process (can be modified) * @param claim_token token to use for access control * @param refund_delay refund delay * @param inventory_products_length length of the @a inventory_products array * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products * @return MHD result code */ static MHD_RESULT patch_order (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, const struct GNUNET_HashCode *h_post_data, json_t *order, const struct TALER_ClaimTokenP *claim_token, struct GNUNET_TIME_Relative refund_delay, unsigned int inventory_products_length, const struct InventoryProduct inventory_products[], unsigned int uuids_length, const struct GNUNET_Uuid uuids[]) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &hc->instance->settings; const char *order_id = NULL; const char *fulfillment_url = NULL; const char *merchant_base_url = NULL; json_t *jmerchant = NULL; json_t *delivery_location = NULL; struct TALER_Amount max_wire_fee = { 0 }; struct TALER_Amount max_fee = { 0 }; uint32_t wire_fee_amortization = 0; struct GNUNET_TIME_Absolute timestamp = { 0 }; struct GNUNET_TIME_Absolute delivery_date = { 0 }; struct GNUNET_TIME_Absolute refund_deadline = GNUNET_TIME_UNIT_FOREVER_ABS; struct GNUNET_TIME_Absolute pay_deadline = { 0 }; struct GNUNET_TIME_Absolute wire_deadline = GNUNET_TIME_UNIT_FOREVER_ABS; /* auto_refund only needs to be type-checked, * mostly because in GNUnet relative times can't * be negative. */ struct GNUNET_TIME_Relative auto_refund; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("merchant_base_url", &merchant_base_url)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("merchant", &jmerchant)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("order_id", &order_id)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_absolute_time ("timestamp", ×tamp)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_absolute_time ("refund_deadline", &refund_deadline)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_absolute_time ("pay_deadline", &pay_deadline)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_absolute_time ("wire_transfer_deadline", &wire_deadline)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount ("max_fee", TMH_currency, &max_fee)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount ("max_wire_fee", TMH_currency, &max_wire_fee)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("wire_fee_amortization", &wire_fee_amortization)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_absolute_time ("delivery_date", &delivery_date)), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_relative_time ("auto_refund", &auto_refund)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("delivery_location", &delivery_location)), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; ret = TALER_MHD_parse_json_data (connection, order, spec); if (GNUNET_OK != ret) { GNUNET_break_op (0); return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } /* Add order_id if it doesn't exist. */ if (NULL == order_id) { char buf[256]; time_t timer; struct tm *tm_info; size_t off; uint64_t rand; char *last; json_t *jbuf; time (&timer); tm_info = localtime (&timer); if (NULL == tm_info) { return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, NULL); } off = strftime (buf, sizeof (buf) - 1, "%Y.%j", tm_info); /* Check for error state of strftime */ GNUNET_assert (0 != off); buf[off++] = '-'; rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, UINT64_MAX); last = GNUNET_STRINGS_data_to_string (&rand, sizeof (uint64_t), &buf[off], sizeof (buf) - off); GNUNET_assert (NULL != last); *last = '\0'; jbuf = json_string (buf); GNUNET_assert (NULL != jbuf); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Assigning order ID `%s' server-side\n", buf); GNUNET_break (0 == json_object_set_new (order, "order_id", jbuf)); order_id = json_string_value (jbuf); GNUNET_assert (NULL != order_id); } /* Patch fulfillment URL with order_id (implements #6467). */ if (NULL != fulfillment_url) { const char *pos; pos = strstr (fulfillment_url, "${ORDER_ID}"); if (NULL != pos) { /* replace ${ORDER_ID} with the real order_id */ char *nurl; /* We only allow one placeholder */ if (strstr (pos + strlen ("${ORDER_ID}"), "${ORDER_ID}")) { /* FIXME: free anything? */ GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "fulfillment_url"); } GNUNET_asprintf (&nurl, "%.*s%s%s", /* first output URL until ${ORDER_ID} */ (int) (pos - fulfillment_url), fulfillment_url, /* replace ${ORDER_ID} with the right order_id */ order_id, /* append rest of original URL */ pos + strlen ("${ORDER_ID}")); /* replace in JSON of the order */ GNUNET_break (0 == json_object_set_new (order, "fulfillment_url", json_string (nurl))); GNUNET_free (nurl); } } /* Check soundness of refund deadline, and that a timestamp * is actually present. */ { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); (void) GNUNET_TIME_round_abs (&now); /* Add timestamp if it doesn't exist (or is zero) */ if (0 == timestamp.abs_value_us) { GNUNET_assert (0 == json_object_set_new (order, "timestamp", GNUNET_JSON_from_time_abs (now))); } /* If no refund_deadline given, set one based on refund_delay. */ if (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us == refund_deadline.abs_value_us) { if (0 == refund_delay.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Refund delay is zero, no refunds are possible for this order\n"); refund_deadline = now; /* if delay was 0, ensure that refund_deadline == timestamp */ } else { refund_deadline = GNUNET_TIME_relative_to_absolute (refund_delay); (void) GNUNET_TIME_round_abs (&refund_deadline); } GNUNET_assert (0 == json_object_set_new (order, "refund_deadline", GNUNET_JSON_from_time_abs ( refund_deadline))); } if ((0 != delivery_date.abs_value_us) && (delivery_date.abs_value_us < now.abs_value_us) ) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, NULL); } } if (0 == pay_deadline.abs_value_us) { struct GNUNET_TIME_Absolute t; t = GNUNET_TIME_relative_to_absolute (settings->default_pay_delay); (void) GNUNET_TIME_round_abs (&t); GNUNET_assert (0 == json_object_set_new (order, "pay_deadline", GNUNET_JSON_from_time_abs (t))); } if (GNUNET_TIME_UNIT_FOREVER_ABS.abs_value_us == wire_deadline.abs_value_us) { struct GNUNET_TIME_Absolute t; t = GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_relative_max (settings->default_wire_transfer_delay, refund_delay)); wire_deadline = GNUNET_TIME_absolute_max (refund_deadline, t); (void) GNUNET_TIME_round_abs (&wire_deadline); GNUNET_assert (0 == json_object_set_new (order, "wire_transfer_deadline", GNUNET_JSON_from_time_abs ( wire_deadline))); } if (wire_deadline.abs_value_us < refund_deadline.abs_value_us) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, "order:wire_transfer_deadline;order:refund_deadline"); } /* Note: total amount currency match checked later in execute_order() */ if (GNUNET_OK != TALER_amount_is_valid (&max_wire_fee)) { GNUNET_assert (0 == json_object_set_new ( order, "max_wire_fee", TALER_JSON_from_amount (&settings->default_max_wire_fee))); } if (GNUNET_OK != TALER_amount_is_valid (&max_fee)) { GNUNET_assert (0 == json_object_set_new ( order, "max_fee", TALER_JSON_from_amount (&settings->default_max_deposit_fee))); } if (0 == wire_fee_amortization) { GNUNET_assert (0 == json_object_set_new ( order, "wire_fee_amortization", json_integer ( (json_int_t) settings->default_wire_fee_amortization))); } if (NULL == merchant_base_url) { char *url; url = make_merchant_base_url (connection, settings->id); GNUNET_assert (0 == json_object_set_new (order, "merchant_base_url", json_string (url))); GNUNET_free (url); } else if (('\0' == *merchant_base_url) || ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, "merchant_base_url is not valid"); } /* Fill in merchant information if necessary */ if (NULL != jmerchant) { GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, "'merchant' field already set, but must be provided by backend"); } jmerchant = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", settings->name)); GNUNET_assert (NULL != jmerchant); { json_t *loca; /* Handle merchant address */ loca = settings->address; if (NULL != loca) { loca = json_deep_copy (loca); GNUNET_assert (0 == json_object_set_new (jmerchant, "address", loca)); } } { json_t *locj; /* Handle merchant jurisdiction */ locj = settings->jurisdiction; if (NULL != locj) { locj = json_deep_copy (locj); GNUNET_assert (0 == json_object_set_new (jmerchant, "jurisdiction", locj)); } } GNUNET_assert (0 == json_object_set_new (order, "merchant", jmerchant)); /* add fields to the contract that the backend should provide */ GNUNET_assert (0 == json_object_set (order, "exchanges", TMH_trusted_exchanges)); GNUNET_assert (0 == json_object_set (order, "auditors", j_auditors)); GNUNET_assert (0 == json_object_set_new (order, "merchant_pub", GNUNET_JSON_from_data_auto ( &hc->instance->merchant_pub))); if (GNUNET_OK != TALER_JSON_contract_seed_forgettable (order)) { return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_JSON_INVALID, "could not compute hash of order due to bogus forgettable fields"); } if ( (NULL != delivery_location) && (! TMH_location_object_valid (delivery_location)) ) { 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, "delivery_location"); } /* sanity check result */ { struct GNUNET_HashCode h_control; switch (TALER_JSON_contract_hash (order, &h_control)) { case GNUNET_SYSERR: GNUNET_break (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, "could not compute hash of patched order"); case GNUNET_NO: GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, "order contained unallowed values"); case GNUNET_OK: break; } } return execute_order (connection, hc, h_post_data, order, claim_token, inventory_products_length, inventory_products, uuids_length, uuids); } /** * Process the @a payment_target and add the details of how the * order could be paid to @a order. On success, continue * processing with patch_order(). * * @param connection connection to write the result or error to * @param hc handler context for the request * @param h_post_data hash of the client's POST request, for idempotency checks * @param[in,out] order order to process (can be modified) * @param claim_token token to use for access control * @param refund_delay refund delay * @param payment_target desired wire method, NULL for no preference * @param inventory_products_length length of the @a inventory_products array * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products * @return MHD result code */ static MHD_RESULT add_payment_details (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, const struct GNUNET_HashCode *h_post_data, json_t *order, const struct TALER_ClaimTokenP *claim_token, struct GNUNET_TIME_Relative refund_delay, const char *payment_target, unsigned int inventory_products_length, const struct InventoryProduct inventory_products[], unsigned int uuids_length, const struct GNUNET_Uuid uuids[]) { struct TMH_WireMethod *wm; wm = hc->instance->wm_head; /* Locate wire method that has a matching payment target */ while ( (NULL != wm) && ( (! wm->active) || ( (NULL != payment_target) && (0 != strcasecmp (payment_target, wm->wire_method) ) ) ) ) wm = wm->next; if (NULL == wm) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No wire method available for instance '%s'\n", hc->instance->settings.id); return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, payment_target); } GNUNET_assert (0 == json_object_set_new (order, "h_wire", GNUNET_JSON_from_data_auto ( &wm->h_wire))); GNUNET_assert (0 == json_object_set_new (order, "wire_method", json_string (wm->wire_method))); return patch_order (connection, hc, h_post_data, order, claim_token, refund_delay, inventory_products_length, inventory_products, uuids_length, uuids); } /** * Merge the inventory products into @a order, querying the * database about the details of those products. Upon success, * continue processing by calling add_payment_details(). * * @param connection connection to write the result or error to * @param hc handler context for the request * @param h_post_data hash of the client's POST request, for idempotency checks * @param[in,out] order order to process (can be modified) * @param claim_token token to use for access control * @param refund_delay time window where it is possible to ask a refund * @param payment_target RFC8905 payment target type to find a matching merchant account * @param inventory_products_length length of the @a inventory_products array * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products * @return MHD result code */ static MHD_RESULT merge_inventory (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, const struct GNUNET_HashCode *h_post_data, json_t *order, const struct TALER_ClaimTokenP *claim_token, struct GNUNET_TIME_Relative refund_delay, const char *payment_target, unsigned int inventory_products_length, const struct InventoryProduct inventory_products[], unsigned int uuids_length, const struct GNUNET_Uuid uuids[]) { /** * inventory_products => instructions to add products to contract terms * order.products => contains products that are not from the backend-managed inventory. */ GNUNET_assert (NULL != order); { json_t *jprod = json_object_get (order, "products"); if (NULL == jprod) { GNUNET_assert (0 == json_object_set_new (order, "products", json_array ())); } else if (! json_is_array (jprod)) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "order.products"); } } /* Populate products from inventory product array and database */ { json_t *np = json_array (); for (unsigned int i = 0; ilookup_product (TMH_db->cls, hc->instance->settings.id, inventory_products[i].product_id, &pd); if (qs <= 0) { enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; unsigned int http_status = 0; switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_FETCH_FAILED; break; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; break; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: http_status = MHD_HTTP_NOT_FOUND; ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* case listed to make compilers happy */ GNUNET_assert (0); } json_decref (np); return TALER_MHD_reply_with_error (connection, http_status, ec, inventory_products[i].product_id); } { json_t *p; p = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("description", pd.description), GNUNET_JSON_pack_object_steal ("description_i18n", pd.description_i18n), GNUNET_JSON_pack_string ("unit", pd.unit), TALER_JSON_pack_amount ("price", &pd.price), GNUNET_JSON_pack_array_steal ("taxes", pd.taxes), GNUNET_JSON_pack_string ("image", pd.image), GNUNET_JSON_pack_uint64 ("quantity", inventory_products[i]. quantity)); GNUNET_assert (NULL != p); GNUNET_assert (0 == json_array_append_new (np, p)); } GNUNET_free (pd.description); GNUNET_free (pd.unit); GNUNET_free (pd.image); json_decref (pd.address); } /* merge into existing products list */ { json_t *xp; xp = json_object_get (order, "products"); GNUNET_assert (NULL != xp); json_array_extend (xp, np); json_decref (np); } } return add_payment_details (connection, hc, h_post_data, order, claim_token, refund_delay, payment_target, inventory_products_length, inventory_products, uuids_length, uuids); } /** * Generate an order. We add the fields 'exchanges', 'merchant_pub', and * 'H_wire' to the order gotten from the frontend, as well as possibly other * fields if the frontend did not provide them. Returns the order_id. * * @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_orders (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { json_t *order; struct GNUNET_TIME_Relative refund_delay = { .rel_value_us = 0 }; const char *payment_target = NULL; json_t *ip = NULL; unsigned int ips_len = 0; struct InventoryProduct *ips = NULL; unsigned int uuids_len = 0; json_t *uuid; struct GNUNET_Uuid *uuids = NULL; struct TALER_ClaimTokenP claim_token; bool create_token = true; /* default */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("order", &order), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_relative_time ("refund_delay", &refund_delay)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("payment_target", &payment_target)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("inventory_products", &ip)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("lock_uuids", &uuid)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("create_token", &create_token)), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; struct GNUNET_HashCode h_post_data; (void) rh; ret = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Refund delay is %s\n", GNUNET_STRINGS_relative_time_to_string (refund_delay, GNUNET_NO)); TMH_db->expire_locks (TMH_db->cls); if (create_token) { GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &claim_token, sizeof (claim_token)); } else { /* we use all-zeros for 'no token' */ memset (&claim_token, 0, sizeof (claim_token)); } /* Compute h_post_data (for idempotency check) */ { char *req_body_enc; /* Dump normalized JSON to string. */ if (NULL == (req_body_enc = json_dumps (hc->request_body, JSON_ENCODE_ANY | JSON_COMPACT | JSON_SORT_KEYS))) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_ALLOCATION_FAILURE, "request body normalization for hashing"); } GNUNET_CRYPTO_hash (req_body_enc, strlen (req_body_enc), &h_post_data); GNUNET_free (req_body_enc); } /* parse the inventory_products (optionally given) */ if (NULL != ip) { if (! json_is_array (ip)) { GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "inventory_products"); } GNUNET_array_grow (ips, ips_len, json_array_size (ip)); for (unsigned int i = 0; i