diff options
Diffstat (limited to 'src/backend/taler-merchant-httpd_private-post-orders.c')
-rw-r--r-- | src/backend/taler-merchant-httpd_private-post-orders.c | 3282 |
1 files changed, 2298 insertions, 984 deletions
diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c index bb64fb02..eedece55 100644 --- a/src/backend/taler-merchant-httpd_private-post-orders.c +++ b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014, 2015, 2016, 2018, 2020, 2021 Taler Systems SA + (C) 2014-2023 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 @@ -22,16 +22,25 @@ * @brief the POST /orders handler * @author Christian Grothoff * @author Marcello Stanisci + * @author Christian Blättler */ #include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_time_lib.h> #include <jansson.h> +#include <microhttpd.h> +#include <string.h> +#include <taler/taler_error_codes.h> #include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> +#include "taler-merchant-httpd.h" #include "taler-merchant-httpd_private-post-orders.h" -#include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_contract.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_private-get-orders.h" +#include "taler_merchantdb_plugin.h" /** @@ -40,6 +49,11 @@ #define MAX_RETRIES 3 /** + * Maximum number of inventory products per order. + */ +#define MAX_PRODUCTS 1024 + +/** * What is the label under which we find/place the merchant's * jurisdiction in the locations list by default? */ @@ -53,109 +67,6 @@ /** - * 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 *product_id = NULL; - uint64_t quantity; - const char *unit = NULL; - struct TALER_Amount price; - const char *image = NULL; - json_t *taxes = NULL; - struct GNUNET_TIME_Timestamp delivery_date; - const char *error_name; - unsigned int error_line; - enum GNUNET_GenericReturnValue res; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("product_id", - &product_id), - NULL), - TALER_JSON_spec_i18n_str ("description", - &description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("quantity", - &quantity), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit", - &unit), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount ("price", - TMH_currency, - &price), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("image", - &image), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("taxes", - &taxes), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &delivery_date), - NULL), - 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_WARNING, - "Product parsing failed at #%u: %s:%u\n", - (unsigned int) index, - error_name, - error_line); - return GNUNET_SYSERR; - } - if ( (NULL != taxes) && - (! TMH_taxes_array_valid (taxes) ) ) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Product parsing failed for taxes\n"); - GNUNET_JSON_parse_free (spec); - return GNUNET_SYSERR; - } - if ( (NULL != image) && - (! TMH_image_data_url_valid (image) ) ) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Product parsing failed for image\n"); - GNUNET_JSON_parse_free (spec); - 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 @@ -166,46 +77,13 @@ 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 }; + struct GNUNET_Buffer buf; - 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) - GNUNET_buffer_write_path (&buf, uri_path); - - if (0 != strcmp (instance_id, - "default")) - { - GNUNET_buffer_write_path (&buf, - "/instances/"); - GNUNET_buffer_write_str (&buf, - instance_id); - } + if (GNUNET_OK != + TMH_base_url_by_connection (connection, + instance_id, + &buf)) + return NULL; GNUNET_buffer_write_path (&buf, ""); return GNUNET_buffer_reap_str (&buf); @@ -231,33 +109,607 @@ struct InventoryProduct /** + * Handle for a rekey operation where we (re)request + * the /keys from the exchange. + */ +struct RekeyExchange +{ + /** + * Kept in a DLL. + */ + struct RekeyExchange *prev; + + /** + * Kept in a DLL. + */ + struct RekeyExchange *next; + + /** + * order this is for. + */ + struct OrderContext *oc; + + /** + * Base URL of the exchange. + */ + char *url; + + /** + * Request for keys. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + +}; + + +/** + * Information we keep per order we are processing. + */ +struct OrderContext +{ + /** + * Information set in the ORDER_PHASE_PARSE_REQUEST phase. + */ + struct + { + /** + * Order field of the request + */ + json_t *order; + + /** + * Set to how long refunds will be allowed. + */ + struct GNUNET_TIME_Relative refund_delay; + + /** + * RFC8905 payment target type to find a matching merchant account + */ + const char *payment_target; + + /** + * Shared key to use with @e pos_algorithm. + */ + const char *pos_key; + + /** + * Selected algorithm (by template) when we are to + * generate an OTP code for payment confirmation. + */ + enum TALER_MerchantConfirmationAlgorithm pos_algorithm; + + /** + * Hash of the POST request data, used to detect + * idempotent requests. + */ + struct TALER_MerchantPostDataHashP h_post_data; + + /** + * Length of the @e inventory_products array. + */ + unsigned int inventory_products_length; + + /** + * Specifies that some products are to be included in the + * order from the inventory. For these inventory management + * is performed (so the products must be in stock). + */ + struct InventoryProduct *inventory_products; + + /** + * Length of the @e uuids array. + */ + unsigned int uuids_length; + + /** + * array of UUIDs used to reserve products from @a inventory_products. + */ + struct GNUNET_Uuid *uuids; + + /** + * Claim token for the request. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * Session ID (optional) to use for the order. + */ + const char *session_id; + + } parse_request; + + + /** + * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase. + */ + struct + { + /** + * Wire method (and our bank account) we have selected + * to be included for this order. + */ + const struct TMH_WireMethod *wm; + } add_payment_details; + + /** + * Information set in the ORDER_PHASE_PARSE_ORDER phase. + */ + struct + { + /** + * Version of the contract terms. + */ + enum TALER_MerchantContractVersion version; + + /** + * Our order ID. + */ + const char *order_id; + + /** + * Summary of the contract. + */ + const char *summary; + + /** + * Internationalized summary. + */ + json_t *summary_i18n; + + /** + * URL that will show that the contract was successful + * after it has been paid for. + */ + const char *fulfillment_url; + + /** + * Message shown to the customer after paying for the contract. + * Either fulfillment_url or fulfillment_message must be specified. + */ + const char *fulfillment_message; + + /** + * Map from IETF BCP 47 language tags to localized fulfillment messages. + */ + json_t *fulfillment_message_i18n; + + /** + * Array of products that are part of the purchase. + */ + const json_t *products; + + /** + * URL where the same contract could be ordered again (if available). + */ + const char *public_reorder_url; + + /** + * Array of contract choices. Is null for v0 contracts. + */ + const json_t *choices; + + /** + * Merchant base URL. + */ + char *merchant_base_url; + + /** + * Timestamp of the order. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Deadline for refunds. + */ + struct GNUNET_TIME_Timestamp refund_deadline; + + /** + * Payment deadline. + */ + struct GNUNET_TIME_Timestamp pay_deadline; + + /** + * Wire transfer deadline. + */ + struct GNUNET_TIME_Timestamp wire_deadline; + + /** + * Delivery date. + */ + struct GNUNET_TIME_Timestamp delivery_date; + + /** + * Delivery location. + */ + json_t *delivery_location; + + /** + * Gross amount value of the contract. Used to + * compute @e max_stefan_fee. + */ + struct TALER_Amount brutto; + + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; + + /** + * Specifies for how long the wallet should try to get an + * automatic refund for the purchase. + */ + struct GNUNET_TIME_Relative auto_refund; + + /** + * Nonce generated by the wallet and echoed by the merchant + * in this field when the proposal is generated. + */ + const char *nonce; + + /** + * Extra data that is only interpreted by the merchant frontend. + */ + const json_t *extra; + + /** + * Minimum age required by the order. + */ + uint32_t minimum_age; + + } parse_order; + + /** + * Information set in the ORDER_PHASE_PARSE_CHOICES phase. + */ + struct + { + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MerchantContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Array of token types referenced in the contract. + */ + struct TALER_MerchantContractTokenAuthority *authorities; + + /** + * Length of the @e authorities array. + */ + unsigned int authorities_len; + } parse_choices; + + /** + * Information set in the ORDER_PHASE_MERGE_INVENTORY phase. + */ + struct + { + /** + * Merged array of products in the @e order. + */ + json_t *products; + } merge_inventory; + + /** + * Information set in the ORDER_PHASE_SET_EXCHANGES phase. + */ + struct + { + /** + * Array of exchanges we find acceptable for this + * order. + */ + json_t *exchanges; + + /** + * Forced requests to /keys to update our exchange + * information. + */ + struct RekeyExchange *pending_reload_head; + + /** + * Forced requests to /keys to update our exchange + * information. + */ + struct RekeyExchange *pending_reload_tail; + + /** + * Did we previously force reloading of /keys from + * all exchanges? Set to 'true' to prevent us from + * doing it again (and again...). + */ + bool forced_reload; + + /** + * Set to true once we are sure that we have at + * least one good exchange. + */ + bool exchange_good; + + /** + * Maximum fee for @e order based on STEFAN curves. + * Used to set @e max_fee if not provided as part of + * @e order. + */ + struct TALER_Amount max_stefan_fee; + } set_exchanges; + + /** + * Information set in the ORDER_PHASE_SET_MAX_FEE phase. + */ + struct + { + /** + * Maximum fee + */ + struct TALER_Amount max_fee; + } set_max_fee; + + /** + * Information set in the ORDER_PHASE_EXECUTE_ORDER phase. + */ + struct + { + /** + * Which product (by offset) is out of stock, UINT_MAX if all were in-stock. + */ + unsigned int out_of_stock_index; + + /** + * Set to a previous claim token *if* @e idempotent + * is also true. + */ + struct TALER_ClaimTokenP token; + + /** + * Set to true if the order was idempotent and there + * was an equivalent one before. + */ + bool idempotent; + + /** + * Set to true if the order is in conflict with a + * previous order with the same order ID. + */ + bool conflict; + } execute_order; + + struct + { + /** + * Contract terms to store in the database. + */ + json_t *contract; + } serialize_order; + + /** + * Connection of the request. + */ + struct MHD_Connection *connection; + + /** + * Kept in a DLL while suspended. + */ + struct OrderContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct OrderContext *prev; + + /** + * Handler context for the request. + */ + struct TMH_HandlerContext *hc; + + /** + * #GNUNET_YES if suspended. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Current phase of setting up the order. + */ + enum + { + ORDER_PHASE_PARSE_REQUEST, + ORDER_PHASE_PARSE_ORDER, + ORDER_PHASE_PARSE_CHOICES, + ORDER_PHASE_MERGE_INVENTORY, + ORDER_PHASE_ADD_PAYMENT_DETAILS, + ORDER_PHASE_SET_EXCHANGES, + ORDER_PHASE_SET_MAX_FEE, + ORDER_PHASE_SERIALIZE_ORDER, + ORDER_PHASE_SALT_FORGETTABLE, + ORDER_PHASE_CHECK_CONTRACT, + ORDER_PHASE_EXECUTE_ORDER, + + /** + * Processing is done, we should return #MHD_YES. + */ + ORDER_PHASE_FINISHED_MHD_YES, + + /** + * Processing is done, we should return #MHD_NO. + */ + ORDER_PHASE_FINISHED_MHD_NO + } phase; + + +}; + + +/** + * Kept in a DLL while suspended. + */ +static struct OrderContext *oc_head; + +/** + * Kept in a DLL while suspended. + */ +static struct OrderContext *oc_tail; + + +void +TMH_force_orders_resume () +{ + struct OrderContext *oc; + + while (NULL != (oc = oc_head)) + { + GNUNET_CONTAINER_DLL_remove (oc_head, + oc_tail, + oc); + oc->suspended = GNUNET_SYSERR; + MHD_resume_connection (oc->connection); + } +} + + +/** + * Update the phase of @a oc based on @a mret. + * + * @param[in,out] oc order to update phase for + * @param mret #MHD_NO to close with #MHD_NO + * #MHD_YES to close with #MHD_YES + */ +static void +finalize_order (struct OrderContext *oc, + MHD_RESULT mret) +{ + oc->phase = (MHD_YES == mret) + ? ORDER_PHASE_FINISHED_MHD_YES + : ORDER_PHASE_FINISHED_MHD_NO; +} + + +/** + * Update the phase of @a oc based on @a ret. + * + * @param[in,out] oc order to update phase for + * @param ret #GNUNET_SYSERR to close with #MHD_NO + * #GNUNET_NO to close with #MHD_YES + * #GNUNET_OK is not allowed! + */ +static void +finalize_order2 (struct OrderContext *oc, + enum GNUNET_GenericReturnValue ret) +{ + GNUNET_assert (GNUNET_OK != ret); + oc->phase = (GNUNET_NO == ret) + ? ORDER_PHASE_FINISHED_MHD_YES + : ORDER_PHASE_FINISHED_MHD_NO; +} + + +/** + * Generate an error response for @a oc. + * + * @param[in,out] oc order context to respond to + * @param http_status HTTP status code to set + * @param ec error code to set + * @param detail error message detail to set + */ +static void +reply_with_error (struct OrderContext *oc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail) +{ + MHD_RESULT mret; + + mret = TALER_MHD_reply_with_error (oc->connection, + http_status, + ec, + detail); + finalize_order (oc, + mret); +} + + +/** + * Clean up memory used by @a cls. + * + * @param[in] cls the `struct OrderContext` to clean up + */ +static void +clean_order (void *cls) +{ + struct OrderContext *oc = cls; + struct RekeyExchange *rx; + + while (NULL != (rx = oc->set_exchanges.pending_reload_head)) + { + GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + TMH_EXCHANGES_keys4exchange_cancel (rx->fo); + GNUNET_free (rx->url); + GNUNET_free (rx); + } + if (NULL != oc->set_exchanges.exchanges) + { + json_decref (oc->set_exchanges.exchanges); + oc->set_exchanges.exchanges = NULL; + } + if (NULL != oc->parse_order.fulfillment_message_i18n) + { + json_decref (oc->parse_order.fulfillment_message_i18n); + oc->parse_order.fulfillment_message_i18n = NULL; + } + if (NULL != oc->parse_order.summary_i18n) + { + json_decref (oc->parse_order.summary_i18n); + oc->parse_order.summary_i18n = NULL; + } + if (NULL != oc->parse_order.delivery_location) + { + json_decref (oc->parse_order.delivery_location); + oc->parse_order.delivery_location = NULL; + } + if (NULL != oc->merge_inventory.products) + { + json_decref (oc->merge_inventory.products); + oc->merge_inventory.products = NULL; + } + // TODO: Check if this is even correct + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + GNUNET_array_grow (oc->parse_choices.choices[i].inputs, + oc->parse_choices.choices[i].inputs_len, + 0); + GNUNET_array_grow (oc->parse_choices.choices[i].outputs, + oc->parse_choices.choices[i].outputs_len, + 0); + } + GNUNET_array_grow (oc->parse_request.inventory_products, + oc->parse_request.inventory_products_length, + 0); + GNUNET_array_grow (oc->parse_request.uuids, + oc->parse_request.uuids_length, + 0); + json_decref (oc->parse_request.order); + json_decref (oc->serialize_order.contract); + GNUNET_free (oc->parse_order.merchant_base_url); + GNUNET_free (oc); +} + + +/** * 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 + * @param[in,out] oc order context + * @return transaction status, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS 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 TALER_MerchantPostDataHashP *h_post_data, - struct GNUNET_TIME_Timestamp 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) +execute_transaction (struct OrderContext *oc) { enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Timestamp timestamp; @@ -270,14 +722,56 @@ execute_transaction (struct TMH_HandlerContext *hc, GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } + + /* Test if we already have an order with this id */ + { + json_t *contract_terms; + struct TALER_MerchantPostDataHashP orig_post; + + qs = TMH_db->lookup_order (TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + &oc->execute_order.token, + &orig_post, + &contract_terms); + /* If yes, check for idempotency */ + if (0 > qs) + { + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + TMH_db->rollback (TMH_db->cls); + 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, + &oc->parse_request.h_post_data)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation idempotent\n"); + oc->execute_order.idempotent = true; + return qs; + } + GNUNET_break_op (0); + oc->execute_order.conflict = true; + return qs; + } + } + /* Setup order */ qs = TMH_db->insert_order (TMH_db->cls, - hc->instance->settings.id, - order_id, - h_post_data, - pay_deadline, - claim_token, - order); // called 'contract terms' at database. + oc->hc->instance->settings.id, + oc->parse_order.order_id, + oc->parse_request.session_id, + &oc->parse_request.h_post_data, + oc->parse_order.pay_deadline, + &oc->parse_request.claim_token, + oc->serialize_order.contract, /* called 'contract terms' at database. */ + oc->parse_request.pos_key, + oc->parse_request.pos_algorithm); if (qs <= 0) { /* qs == 0: probably instance does not exist (anymore) */ @@ -285,10 +779,10 @@ execute_transaction (struct TMH_HandlerContext *hc, return qs; } /* Migrate locks from UUIDs to new order: first release old locks */ - for (unsigned int i = 0; i<uuids_length; i++) + for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) { qs = TMH_db->unlock_inventory (TMH_db->cls, - &uuids[i]); + &oc->parse_request.uuids[i]); if (qs < 0) { TMH_db->rollback (TMH_db->cls); @@ -301,13 +795,14 @@ execute_transaction (struct TMH_HandlerContext *hc, (note: this can basically ONLY fail on serializability OR because the UUID locks were insufficient for the desired quantities). */ - for (unsigned int i = 0; i<inventory_products_length; i++) + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) { - qs = TMH_db->insert_order_lock (TMH_db->cls, - hc->instance->settings.id, - order_id, - inventory_products[i].product_id, - inventory_products[i].quantity); + qs = TMH_db->insert_order_lock ( + TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + oc->parse_request.inventory_products[i].product_id, + oc->parse_request.inventory_products[i].quantity); if (qs < 0) { TMH_db->rollback (TMH_db->cls); @@ -317,17 +812,17 @@ execute_transaction (struct TMH_HandlerContext *hc, { /* 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 */ + oc->execute_order.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; + oc->execute_order.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, + oc->hc->instance->settings.id, + oc->parse_order.order_id, ×tamp, &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) @@ -335,7 +830,7 @@ execute_transaction (struct TMH_HandlerContext *hc, TMH_db->rollback (TMH_db->cls); return qs; } - TMH_notify_order_change (hc->instance, + TMH_notify_order_change (oc->hc->instance, TMH_OSF_NONE, timestamp, order_serial); @@ -353,268 +848,123 @@ execute_transaction (struct TMH_HandlerContext *hc, * 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 + * @param[in,out] oc order context */ -static MHD_RESULT -execute_order (struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - const struct TALER_MerchantPostDataHashP *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[]) +static void +execute_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = - &hc->instance->settings; - struct TALER_Amount total; - const char *order_id; - const char *summary; - const char *fulfillment_msg = NULL; - json_t *products; - json_t *merchant; - json_t *summary_i18n = NULL; - json_t *fulfillment_i18n = NULL; - struct GNUNET_TIME_Timestamp timestamp; - struct GNUNET_TIME_Timestamp refund_deadline = { 0 }; - struct GNUNET_TIME_Timestamp wire_transfer_deadline; - struct GNUNET_TIME_Timestamp 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), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("summary_i18n", - &summary_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_message", - &fulfillment_msg), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("fulfillment_message_i18n", - &fulfillment_i18n), - NULL), - GNUNET_JSON_spec_timestamp ("timestamp", - ×tamp), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &refund_deadline), - NULL), - GNUNET_JSON_spec_timestamp ("pay_deadline", - &pay_deadline), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &wire_transfer_deadline), - GNUNET_JSON_spec_end () - }; + &oc->hc->instance->settings; 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"); - } - - if ( (NULL != fulfillment_i18n) && - (! TALER_JSON_check_i18n (fulfillment_i18n)) ) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order:fulfillment_message_i18n"); - } - if ( (NULL != summary_i18n) && - (! TALER_JSON_check_i18n (summary_i18n)) ) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order:summary_i18n"); - } - - /* Test if we already have an order with this id */ - { - struct TALER_ClaimTokenP token; - json_t *contract_terms; - struct TALER_MerchantPostDataHashP 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, + oc->parse_order.order_id, settings->id); for (unsigned int i = 0; i<MAX_RETRIES; i++) { TMH_db->preflight (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); + qs = execute_transaction (oc); 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); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + return; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* should be: contract (!) with same order ID already exists */ - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, - order_id); + oc->parse_order.order_id); + return; } /* Other hard transaction error (disk full, etc.) */ GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); + return; + } + + /* DB transaction succeeded, check for idempotent */ + if (oc->execute_order.idempotent) + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_varsize ( + "token", + GNUNET_is_zero (&oc->execute_order.token) + ? NULL + : &oc->execute_order.token, + sizeof (oc->execute_order.token)))); + finalize_order (oc, + ret); + return; + } + if (oc->execute_order.conflict) + { + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, + oc->parse_order.order_id); + return; } /* DB transaction succeeded, check for out-of-stock */ - if (out_of_stock_index < UINT_MAX) + if (oc->execute_order.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; + const struct InventoryProduct *ip; - memset (&pd, 0, sizeof (pd)); + ip = &oc->parse_request.inventory_products[ + oc->execute_order.out_of_stock_index]; + 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, + oc->hc->instance->settings.id, + ip->product_id, &pd); - GNUNET_JSON_parse_free (spec); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation failed: product out of stock\n"); ret = TALER_MHD_REPLY_JSON_PACK ( - connection, + oc->connection, MHD_HTTP_GONE, GNUNET_JSON_pack_string ( "product_id", - inventory_products[out_of_stock_index].product_id), + ip->product_id), GNUNET_JSON_pack_uint64 ( "requested_quantity", - inventory_products[out_of_stock_index].quantity), + ip->quantity), GNUNET_JSON_pack_uint64 ( "available_quantity", pd.total_stock - pd.total_sold - pd.total_lost), @@ -623,188 +973,1037 @@ execute_order (struct MHD_Connection *connection, "restock_expected", pd.next_restock))); TALER_MERCHANTDB_product_details_free (&pd); - return ret; + finalize_order (oc, + ret); + return; 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)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation failed: unknown product out of stock\n"); + finalize_order (oc, + TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_GONE, + GNUNET_JSON_pack_string ( + "product_id", + ip->product_id), + GNUNET_JSON_pack_uint64 ( + "requested_quantity", + ip->quantity), + GNUNET_JSON_pack_uint64 ( + "available_quantity", + 0))); + return; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); + return; case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error ( - connection, + GNUNET_break (0); + reply_with_error ( + oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, NULL); + return; } GNUNET_break (0); - return MHD_NO; - } + oc->phase = ORDER_PHASE_FINISHED_MHD_NO; + return; + } /* end 'out of stock' case */ /* Everything in-stock, generate positive response */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation succeeded\n"); { MHD_RESULT ret; ret = TALER_MHD_REPLY_JSON_PACK ( - connection, + oc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_string ("order_id", - order_id), + oc->parse_order.order_id), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_varsize ( "token", - GNUNET_is_zero (claim_token) + GNUNET_is_zero (&oc->parse_request.claim_token) ? NULL - : claim_token, - sizeof (*claim_token)))); - GNUNET_JSON_parse_free (spec); - return ret; + : &oc->parse_request.claim_token, + sizeof (oc->parse_request.claim_token)))); + finalize_order (oc, + ret); } } /** - * Add missing fields to the order. Upon success, continue + * Check that the contract is now well-formed. 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 + * @param[in,out] oc order context + */ +static void +check_contract (struct OrderContext *oc) +{ + struct TALER_PrivateContractHashP h_control; + + json_dumpf (oc->serialize_order.contract, + stderr, + JSON_INDENT (2)); + switch (TALER_JSON_contract_hash (oc->serialize_order.contract, + &h_control)) + { + case GNUNET_SYSERR: + GNUNET_break (0); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "could not compute hash of serialized order"); + return; + case GNUNET_NO: + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "order contained unallowed values"); + return; + case GNUNET_OK: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Contract hash is %s\n", + GNUNET_h2s (&h_control.hash)); + oc->phase++; + return; + } + GNUNET_assert (0); +} + + +/** + * Modify the final contract terms adding salts for + * items that are forgettable. + * + * @param[in,out] oc order context + */ +static void +salt_forgettable (struct OrderContext *oc) +{ + if (GNUNET_OK != + TALER_JSON_contract_seed_forgettable (oc->parse_request.order, + oc->serialize_order.contract)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_JSON_INVALID, + "could not compute hash of order due to bogus forgettable fields"); + return; + } + oc->phase++; +} + + +/** + * Update MAX STEFAN fees based on @a keys. + * + * @param[in,out] oc order context to update + * @param keys keys to derive STEFAN fees from + */ +static void +update_stefan (struct OrderContext *oc, + const struct TALER_EXCHANGE_Keys *keys) +{ + struct TALER_Amount net; + + if (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_b2n (keys, + &oc->parse_order.brutto, + &net)) + { + struct TALER_Amount fee; + + TALER_EXCHANGE_keys_stefan_round (keys, + &net); + if (-1 == TALER_amount_cmp (&oc->parse_order.brutto, + &net)) + { + /* brutto < netto! */ + /* => after rounding, there is no real difference */ + net = oc->parse_order.brutto; + } + GNUNET_assert (0 <= + TALER_amount_subtract (&fee, + &oc->parse_order.brutto, + &net)); + if ( (GNUNET_OK != + TALER_amount_is_valid (&oc->set_exchanges.max_stefan_fee)) || + (-1 == TALER_amount_cmp (&oc->set_exchanges.max_stefan_fee, + &fee)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Updated STEFAN-based fee to %s\n", + TALER_amount2s (&fee)); + oc->set_exchanges.max_stefan_fee = fee; + } + } +} + + +/** + * Compute the set of exchanges that would be acceptable + * for this order. + * + * @param cls our `struct OrderContext` + * @param url base URL of an exchange (not used) + * @param exchange internal handle for the exchange + */ +static void +get_acceptable (void *cls, + const char *url, + const struct TMH_Exchange *exchange) +{ + struct OrderContext *oc = cls; + unsigned int priority = 42; /* make compiler happy */ + json_t *j_exchange; + enum GNUNET_GenericReturnValue res; + + res = TMH_exchange_check_debit (exchange, + oc->add_payment_details.wm); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s evaluated at %d\n", + url, + res); + switch (res) + { + case GNUNET_OK: + priority = 1024; /* high */ + oc->set_exchanges.exchange_good = true; + break; + case GNUNET_NO: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case GNUNET_SYSERR: + if (oc->set_exchanges.forced_reload) + priority = 256; /* fresh, no accounts yet */ + else + priority = 768; /* stale, no accounts yet */ + break; + } + j_exchange = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("url", + url), + GNUNET_JSON_pack_uint64 ("priority", + priority), + GNUNET_JSON_pack_data_auto ("master_pub", + TMH_EXCHANGES_get_master_pub (exchange))); + GNUNET_assert (NULL != j_exchange); + GNUNET_assert (0 == + json_array_append_new (oc->set_exchanges.exchanges, + j_exchange)); +} + + +/** + * Exchange `/keys` processing is done, resume handling + * the order. + * + * @param[in,out] oc context to resume + */ +static void +resume_with_keys (struct OrderContext *oc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming order processing after /keys downloads (now have %u accounts)\n", + (unsigned int) json_array_size (oc->set_exchanges.exchanges)); + GNUNET_assert (GNUNET_YES == oc->suspended); + GNUNET_CONTAINER_DLL_remove (oc_head, + oc_tail, + oc); + oc->suspended = GNUNET_NO; + MHD_resume_connection (oc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Function called with the result of a #TMH_EXCHANGES_keys4exchange() + * operation. + * + * @param cls closure with our `struct RekeyExchange *` + * @param keys the keys of the exchange + * @param exchange representation of the exchange + */ +static void +keys_cb ( + void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct RekeyExchange *rx = cls; + struct OrderContext *oc = rx->oc; + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + + rx->fo = NULL; + GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + if (NULL == keys) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to download %skeys\n", + rx->url); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response for %skeys\n", + rx->url); + if ( (settings->use_stefan) && + (GNUNET_OK != + TALER_amount_is_valid (&oc->parse_order.max_fee)) ) + update_stefan (oc, + keys); + get_acceptable (oc, + rx->url, + exchange); + } + GNUNET_free (rx->url); + GNUNET_free (rx); + if (NULL != oc->set_exchanges.pending_reload_head) + return; + resume_with_keys (oc); +} + + +/** + * Force re-downloading of /keys from @a exchange, + * we currently have no acceptable exchange, so we + * should try to get one. + * + * @param cls closure with our `struct OrderContext` + * @param url base URL of the exchange + * @param exchange internal handle for the exchange + */ +static void +get_exchange_keys (void *cls, + const char *url, + const struct TMH_Exchange *exchange) +{ + struct OrderContext *oc = cls; + struct RekeyExchange *rx; + + rx = GNUNET_new (struct RekeyExchange); + rx->oc = oc; + rx->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + if (oc->set_exchanges.forced_reload) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Forcing download of %skeys\n", + url); + rx->fo = TMH_EXCHANGES_keys4exchange (url, + oc->set_exchanges.forced_reload, + &keys_cb, + rx); +} + +/** + * Fetch details about the token family with the given @a slug + * and add them to the list of token authorities. Check if the + * token family already has a valid key configured and if not, + * create a new one. + * + * @param[in,out] oc order context + * @param slug slug of the token family + * @param start_date validity start date of the token */ static MHD_RESULT -patch_order (struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - const struct TALER_MerchantPostDataHashP *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[]) +set_token_authority (struct OrderContext *oc, + const char *slug, + struct GNUNET_TIME_Timestamp start_date) +{ + struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; + struct TALER_MerchantContractTokenAuthority authority; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute min_start_date = GNUNET_TIME_absolute_subtract ( + start_date.abs_time, + // TODO: make this configurable. This is the granularity of token + // expiration dates. + GNUNET_TIME_UNIT_DAYS + ); + + qs = TMH_db->lookup_token_family_key (TMH_db->cls, + oc->hc->instance->settings.id, + slug, + GNUNET_TIME_absolute_to_timestamp (min_start_date), + start_date, + &key_details); + + 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: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Token family slug unknown\n"); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } + GNUNET_break (0); + reply_with_error (oc, + http_status, + ec, + "token_family_slug"); + return MHD_NO; + } + + if (NULL == key_details.pub) + { + /* If public key is NULL, private key must also be NULL */ + GNUNET_assert (NULL == key_details.priv); + + struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; + struct GNUNET_CRYPTO_BlindSignPublicKey *pub; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + struct GNUNET_TIME_Timestamp valid_before = GNUNET_TIME_absolute_to_timestamp( + GNUNET_TIME_absolute_add (now, + key_details.token_family.duration)); + + GNUNET_CRYPTO_blind_sign_keys_create (&priv, + &pub, + // TODO: Make cipher and key length configurable + GNUNET_CRYPTO_BSA_RSA, + 4096); + + struct TALER_TokenFamilyPublicKey token_pub = { + .public_key = *pub, + }; + struct TALER_TokenFamilyPrivateKey token_priv = { + .private_key = *priv, + }; + + qs = TMH_db->insert_token_family_key (TMH_db->cls, + slug, + &token_pub, + &token_priv, + GNUNET_TIME_absolute_to_timestamp (now), + valid_before); + + authority.token_expiration = valid_before; + authority.pub = &token_pub; + // GNUNET_CRYPTO_blind_sign_priv_decref (&token_priv.private_key); + } else { + authority.token_expiration = key_details.valid_before; + authority.pub = key_details.pub; + } + + authority.label = slug; + authority.description = key_details.token_family.description; + authority.description_i18n = key_details.token_family.description_i18n; + + GNUNET_free (key_details.token_family.slug); + GNUNET_free (key_details.token_family.name); + if (NULL != key_details.priv) { + GNUNET_CRYPTO_blind_sign_priv_decref (&key_details.priv->private_key); + } + + switch (key_details.token_family.kind) { + case TALER_MERCHANTDB_TFK_Subscription: + authority.kind = TALER_MCTK_SUBSCRIPTION; + authority.details.subscription.start_date = key_details.valid_after; + authority.details.subscription.end_date = key_details.valid_before; + authority.critical = true; + // TODO: Set trusted domains + break; + case TALER_MERCHANTDB_TFK_Discount: + authority.kind = TALER_MCTK_DISCOUNT; + authority.critical = false; + // TODO: Set expected domains + break; + } + + GNUNET_array_append (oc->parse_choices.authorities, + oc->parse_choices.authorities_len, + authority); + + return MHD_YES; +} + +/** + * Serialize order into @a oc->serialize_order.contract, + * ready to be stored in the database. Upon success, continue + * processing with check_contract(). + * + * @param[in,out] oc order context + */ +static void +serialize_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = - &hc->instance->settings; - const char *order_id = NULL; - const char *fulfillment_url = NULL; + &oc->hc->instance->settings; + json_t *merchant; + json_t *token_types = json_object (); + json_t *choices = json_array (); + + merchant = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + settings->name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("website", + settings->website)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("email", + settings->email)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("logo", + settings->logo))); + GNUNET_assert (NULL != merchant); + { + json_t *loca; + + /* Handle merchant address */ + loca = settings->address; + if (NULL != loca) + { + loca = json_deep_copy (loca); + GNUNET_assert (NULL != loca); + GNUNET_assert (0 == + json_object_set_new (merchant, + "address", + loca)); + } + } + { + json_t *juri; + + /* Handle merchant jurisdiction */ + juri = settings->jurisdiction; + if (NULL != juri) + { + juri = json_deep_copy (juri); + GNUNET_assert (NULL != juri); + GNUNET_assert (0 == + json_object_set_new (merchant, + "jurisdiction", + juri)); + } + } + + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices.authorities[i]; + + // TODO: Finish spec to clearly define how token families are stored in + // ContractTerms. + // Here are some thoughts: + // - Multiple keys of the same token family can be referenced in + // one contract. E.g. exchanging old subscription for new. + // - TokenAuthority should be renamed to TokenFamily for consistency. + // - TokenFamilySlug can be used instead of TokenAuthorityLabel, but + // every token-based in- or output needs to have a valid_after date, + // so it's clear with key is referenced. + json_t *jauthority = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + authority->description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + authority->description_i18n)), + GNUNET_JSON_pack_data_auto ("h_pub", + &authority->pub->public_key.pub_key_hash), + GNUNET_JSON_pack_data_auto ("pub", + &authority->pub->public_key.pub_key_hash), + GNUNET_JSON_pack_timestamp ("token_expiration", + authority->token_expiration) + ); + + GNUNET_assert (0 == + json_object_set_new (token_types, + authority->label, + jauthority)); + } + + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + struct TALER_MerchantContractChoice *choice = &oc->parse_choices.choices[i]; + + json_t *inputs = json_array (); + json_t *outputs = json_array (); + + for (unsigned int j = 0; j<choice->inputs_len; j++) + { + struct TALER_MerchantContractInput *input = &choice->inputs[j]; + + json_t *jinput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("type", + input->type) + ); + + if (TALER_MCIT_TOKEN == input->type) + { + GNUNET_assert(0 == + json_object_set_new(jinput, + "number", + json_integer ( + input->details.token.count))); + GNUNET_assert(0 == + json_object_set_new(jinput, + "token_family_slug", + json_string ( + input->details.token.token_family_slug))); + } + + GNUNET_assert (0 == json_array_append_new (inputs, jinput)); + } + + for (unsigned int j = 0; j<choice->outputs_len; j++) + { + struct TALER_MerchantContractOutput *output = &choice->outputs[j]; + + json_t *joutput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("type", + output->type) + ); + + if (TALER_MCOT_TOKEN == output->type) + { + GNUNET_assert(0 == + json_object_set_new(joutput, + "number", + json_integer ( + output->details.token.count))); + + GNUNET_assert(0 == + json_object_set_new(joutput, + "token_family_slug", + json_string ( + output->details.token.token_family_slug))); + } + + GNUNET_assert (0 == json_array_append (outputs, joutput)); + } + + json_t *jchoice = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_incref ("inputs", + inputs), + GNUNET_JSON_pack_array_incref ("outputs", + outputs) + ); + + GNUNET_assert (0 == json_array_append (choices, jchoice)); + } + + oc->serialize_order.contract = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("version", + oc->parse_order.version), + GNUNET_JSON_pack_string ("summary", + oc->parse_order.summary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("summary_i18n", + oc->parse_order.summary_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("public_reorder_url", + oc->parse_order.public_reorder_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_message", + oc->parse_order.fulfillment_message)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("fulfillment_message_i18n", + oc->parse_order.fulfillment_message_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + oc->parse_order.fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_uint64 ("minimum_age", + oc->parse_order.minimum_age)), + GNUNET_JSON_pack_array_incref ("products", + oc->merge_inventory.products), + GNUNET_JSON_pack_data_auto ("h_wire", + &oc->add_payment_details.wm->h_wire), + GNUNET_JSON_pack_string ("wire_method", + oc->add_payment_details.wm->wire_method), + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_timestamp ("timestamp", + oc->parse_order.timestamp), + GNUNET_JSON_pack_timestamp ("pay_deadline", + oc->parse_order.pay_deadline), + GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", + oc->parse_order.wire_deadline), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("delivery_date", + oc->parse_order.delivery_date)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("delivery_location", + oc->parse_order.delivery_location)), + GNUNET_JSON_pack_string ("merchant_base_url", + oc->parse_order.merchant_base_url), + GNUNET_JSON_pack_object_steal ("merchant", + merchant), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &oc->hc->instance->merchant_pub), + GNUNET_JSON_pack_array_incref ("exchanges", + oc->set_exchanges.exchanges), + TALER_JSON_pack_amount ("max_fee", + &oc->set_max_fee.max_fee), + TALER_JSON_pack_amount ("amount", + &oc->parse_order.brutto), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("choices", + choices) + ), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("token_types", + token_types) + ), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("extra", + (json_t *) oc->parse_order.extra)) + ); + + /* Pack does not work here, because it doesn't set zero-values for timestamps */ + GNUNET_assert (0 == + json_object_set_new (oc->serialize_order.contract, + "refund_deadline", + GNUNET_JSON_from_timestamp ( + oc->parse_order.refund_deadline))); + + GNUNET_log ( + GNUNET_ERROR_TYPE_INFO, + "Refund deadline for contact is %llu\n", + (unsigned long long) oc->parse_order.refund_deadline.abs_time.abs_value_us); + GNUNET_log ( + GNUNET_ERROR_TYPE_INFO, + "Wallet timestamp for contact is %llu\n", + (unsigned long long) oc->parse_order.timestamp.abs_time.abs_value_us); + + /* Pack does not work here, because it sets zero-values for relative times */ + /* auto_refund should only be set if it is not 0 */ + if (! GNUNET_TIME_relative_is_zero (oc->parse_order.auto_refund)) + { + GNUNET_assert (0 == + json_object_set_new (oc->serialize_order.contract, + "auto_refund", + GNUNET_JSON_from_time_rel ( + oc->parse_order.auto_refund))); + } + + oc->phase++; +} + +/** + * Set max_fee in @a oc based on STEFAN value if + * not yet present. Upon success, continue + * processing with serialize_order(). + * + * @param[in,out] oc order context + */ +static void +set_max_fee (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + + if (GNUNET_OK != + TALER_amount_is_valid (&oc->parse_order.max_fee)) + { + struct TALER_Amount stefan; + + if ( (settings->use_stefan) && + (GNUNET_OK == + TALER_amount_is_valid (&oc->set_exchanges.max_stefan_fee)) ) + stefan = oc->set_exchanges.max_stefan_fee; + else + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (oc->parse_order.brutto.currency, + &stefan)); + oc->set_max_fee.max_fee = stefan; + } + else + { + oc->set_max_fee.max_fee = oc->parse_order.max_fee; + } + oc->phase++; +} + +/** + * Set list of acceptable exchanges in @a oc. Upon success, continue + * processing with set_max_fee(). + * + * @param[in,out] oc order context + * @return true to suspend execution + */ +static bool +set_exchanges (struct OrderContext *oc) +{ + /* Note: re-building 'oc->exchanges' every time here might be a tad + expensive; could likely consider caching the result if it starts to + matter. */ + if (NULL == oc->set_exchanges.exchanges) + { + oc->set_exchanges.exchanges = json_array (); + GNUNET_assert (NULL != oc->set_exchanges.exchanges); + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + else if (! oc->set_exchanges.exchange_good) + { + if (! oc->set_exchanges.forced_reload) + { + oc->set_exchanges.forced_reload = true; + GNUNET_assert (0 == + json_array_clear (oc->set_exchanges.exchanges)); + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + } + if (NULL != oc->set_exchanges.pending_reload_head) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Still trying to (re)load %skeys\n", + oc->set_exchanges.pending_reload_head->url); + MHD_suspend_connection (oc->connection); + oc->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (oc_head, + oc_tail, + oc); + return true; /* reloads pending */ + } + if (0 == json_array_size (oc->set_exchanges.exchanges)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create order: lacking trusted exchanges\n"); + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, + oc->add_payment_details.wm->wire_method); + return false; + } + if (! oc->set_exchanges.exchange_good) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Creating order, but possibly without usable trusted exchanges\n"); + } + oc->phase++; + return false; +} + + +/** + * Parse the order field of the request. Upon success, continue + * processing with parse_choices(). + * + * @param[in,out] oc order context + */ +static void +parse_order (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; 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_Timestamp timestamp - = GNUNET_TIME_UNIT_ZERO_TS; - struct GNUNET_TIME_Timestamp delivery_date - = GNUNET_TIME_UNIT_ZERO_TS; - struct GNUNET_TIME_Timestamp refund_deadline - = GNUNET_TIME_UNIT_FOREVER_TS; - struct GNUNET_TIME_Timestamp pay_deadline - = GNUNET_TIME_UNIT_ZERO_TS; - struct GNUNET_TIME_Timestamp wire_deadline - = GNUNET_TIME_UNIT_FOREVER_TS; + const char *version = NULL; + const json_t *jmerchant = NULL; /* auto_refund only needs to be type-checked, * mostly because in GNUnet relative times can't * be negative. */ - struct GNUNET_TIME_Relative auto_refund; + bool no_fee; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("merchant_base_url", - &merchant_base_url), + GNUNET_JSON_spec_string ("version", + &version), + NULL), + TALER_JSON_spec_amount_any ("amount", + &oc->parse_order.brutto), + GNUNET_JSON_spec_string ("summary", + &oc->parse_order.summary), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("products", + &oc->parse_order.products), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("merchant", - &jmerchant), + GNUNET_JSON_spec_json ("summary_i18n", + &oc->parse_order.summary_i18n), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("order_id", - &order_id), + &oc->parse_order.order_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("fulfillment_message", + &oc->parse_order.fulfillment_message), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("fulfillment_message_i18n", + &oc->parse_order.fulfillment_message_i18n), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_url", - &fulfillment_url), + &oc->parse_order.fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("public_reorder_url", + &oc->parse_order.public_reorder_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("choices", + &oc->parse_order.choices), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("merchant_base_url", + &merchant_base_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("merchant", + &jmerchant), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("timestamp", - ×tamp), + &oc->parse_order.timestamp), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("refund_deadline", - &refund_deadline), + &oc->parse_order.refund_deadline), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("pay_deadline", - &pay_deadline), + &oc->parse_order.pay_deadline), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &wire_deadline), + &oc->parse_order.wire_deadline), NULL), GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount ("max_fee", - TMH_currency, - &max_fee), - NULL), + TALER_JSON_spec_amount_any ("max_fee", + &oc->parse_order.max_fee), + &no_fee), GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount ("max_wire_fee", - TMH_currency, - &max_wire_fee), + GNUNET_JSON_spec_json ("delivery_location", + &oc->parse_order.delivery_location), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("wire_fee_amortization", - &wire_fee_amortization), + GNUNET_JSON_spec_timestamp ("delivery_date", + &oc->parse_order.delivery_date), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &delivery_date), + GNUNET_JSON_spec_uint32 ("minimum_age", + &oc->parse_order.minimum_age), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("auto_refund", - &auto_refund), + &oc->parse_order.auto_refund), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("delivery_location", - &delivery_location), + GNUNET_JSON_spec_object_const ("extra", + &oc->parse_order.extra), NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; - ret = TALER_MHD_parse_json_data (connection, - order, + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, spec); if (GNUNET_OK != ret) { GNUNET_break_op (0); - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; + finalize_order2 (oc, + ret); + return; + } + if (NULL == version || 0 == strcmp("0", version)) + { + oc->parse_order.version = TALER_MCV_V0; + + if (NULL != oc->parse_order.choices) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, + "choices array must be null for v0 contracts"); + return; + } + } + else if (0 == strcmp("1", version)) + { + oc->parse_order.version = TALER_MCV_V1; + + if (! json_is_array(oc->parse_order.choices)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order.choices is not a valid array"); + return; + } + } + else + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_VERSION_MALFORMED, + "invalid version specified in order, supported are null, '0' or '1'"); + return; + } + if (! TMH_test_exchange_configured_for_currency ( + oc->parse_order.brutto.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "no trusted exchange for this currency"); + return; + } + if ( (! no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.brutto, + &oc->parse_order.max_fee)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; } /* Add order_id if it doesn't exist. */ - if (NULL == order_id) + if (NULL == oc->parse_order.order_id) { char buf[256]; time_t timer; @@ -812,18 +2011,18 @@ patch_order (struct MHD_Connection *connection, size_t off; uint64_t rand; char *last; - json_t *jbuf; time (&timer); tm_info = localtime (&timer); if (NULL == tm_info) { GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, NULL); + return; } off = strftime (buf, sizeof (buf) - 1, @@ -840,25 +2039,21 @@ patch_order (struct MHD_Connection *connection, 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); + + oc->parse_order.order_id = GNUNET_strdup (buf); + GNUNET_assert (NULL != oc->parse_order.order_id); } /* Patch fulfillment URL with order_id (implements #6467). */ - if (NULL != fulfillment_url) + if (NULL != oc->parse_order.fulfillment_url) { const char *pos; - pos = strstr (fulfillment_url, + pos = strstr (oc->parse_order.fulfillment_url, "${ORDER_ID}"); if (NULL != pos) { @@ -870,27 +2065,25 @@ patch_order (struct MHD_Connection *connection, "${ORDER_ID}")) { 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, - "fulfillment_url"); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "fulfillment_url"); + return; } GNUNET_asprintf (&nurl, "%.*s%s%s", /* first output URL until ${ORDER_ID} */ - (int) (pos - fulfillment_url), - fulfillment_url, + (int) (pos - oc->parse_order.fulfillment_url), + oc->parse_order.fulfillment_url, /* replace ${ORDER_ID} with the right order_id */ - order_id, + oc->parse_order.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))); + + oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); + GNUNET_free (nurl); } } @@ -901,437 +2094,507 @@ patch_order (struct MHD_Connection *connection, struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); /* Add timestamp if it doesn't exist (or is zero) */ - if (GNUNET_TIME_absolute_is_zero (timestamp.abs_time)) + if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) { - GNUNET_assert (0 == - json_object_set_new (order, - "timestamp", - GNUNET_JSON_from_timestamp (now))); + oc->parse_order.timestamp = now; } /* If no refund_deadline given, set one based on refund_delay. */ - if (GNUNET_TIME_absolute_is_never (refund_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_never ( + oc->parse_order.refund_deadline.abs_time)) { - if (GNUNET_TIME_relative_is_zero (refund_delay)) + if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) { 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 */ + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; } else { - refund_deadline = GNUNET_TIME_relative_to_timestamp (refund_delay); + oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp ( + oc->parse_request.refund_delay); } - - GNUNET_assert (0 == - json_object_set_new (order, - "refund_deadline", - GNUNET_JSON_from_timestamp ( - refund_deadline))); } - if ( (! GNUNET_TIME_absolute_is_zero (delivery_date.abs_time)) && - (GNUNET_TIME_timestamp_cmp (delivery_date, - <, - now)) ) + + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.delivery_date.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.delivery_date.abs_time)) ) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, NULL); + return; } } - if (GNUNET_TIME_absolute_is_zero (pay_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) { - struct GNUNET_TIME_Timestamp t; + oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( + settings->default_pay_delay); + } + else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST, + NULL); + return; + } - t = GNUNET_TIME_relative_to_timestamp (settings->default_pay_delay); - GNUNET_assert (0 == - json_object_set_new (order, - "pay_deadline", - GNUNET_JSON_from_timestamp (t))); + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.refund_deadline.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.refund_deadline.abs_time)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST, + NULL); + return; } - if (GNUNET_TIME_absolute_is_never (wire_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) { struct GNUNET_TIME_Timestamp t; t = GNUNET_TIME_relative_to_timestamp ( GNUNET_TIME_relative_max (settings->default_wire_transfer_delay, - refund_delay)); - wire_deadline = GNUNET_TIME_timestamp_max (refund_deadline, - t); - if (GNUNET_TIME_absolute_is_never (wire_deadline.abs_time)) + oc->parse_request.refund_delay)); + oc->parse_order.wire_deadline = GNUNET_TIME_timestamp_max ( + oc->parse_order.refund_deadline, + t); + if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, "order:wire_transfer_deadline"); - + return; } - GNUNET_assert (0 == - json_object_set_new (order, - "wire_transfer_deadline", - GNUNET_JSON_from_timestamp ( - wire_deadline))); } - if (GNUNET_TIME_timestamp_cmp (wire_deadline, + if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, <, - refund_deadline)) + oc->parse_order.refund_deadline)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, "order:wire_transfer_deadline;order:refund_deadline"); + return; } - /* 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)) + if (NULL != merchant_base_url) { - 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 (('\0' == *merchant_base_url) || + ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, + "merchant_base_url is not valid"); + return; + } + oc->parse_order.merchant_base_url + = GNUNET_strdup (merchant_base_url); } - if (NULL == merchant_base_url) + else { char *url; - url = make_merchant_base_url (connection, + url = make_merchant_base_url (oc->connection, settings->id); - GNUNET_assert (0 == - json_object_set_new (order, - "merchant_base_url", - json_string (url))); - GNUNET_free (url); + if (NULL == url) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "order:merchant_base_url"); + return; + } + oc->parse_order.merchant_base_url = url; } - else if (('\0' == *merchant_base_url) || - ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) + + if ( (NULL != oc->parse_order.products) && + (! TMH_products_array_valid (oc->parse_order.products)) ) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "merchant_base_url is not valid"); + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order.products"); + return; } - /* Fill in merchant information if necessary */ + /* Merchant information must not already be present */ if (NULL != jmerchant) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, + reply_with_error ( + oc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, "'merchant' field already set, but must be provided by backend"); + return; } + if ( (NULL != oc->parse_order.delivery_location) && + (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) { - json_t *jm; + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "delivery_location"); + return; + } - jm = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - settings->name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - settings->website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("email", - settings->email)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - settings->logo))); - GNUNET_assert (NULL != jm); - { - json_t *loca; + oc->phase++; +} - /* Handle merchant address */ - loca = settings->address; - if (NULL != loca) - { - loca = json_deep_copy (loca); - GNUNET_assert (0 == - json_object_set_new (jm, - "address", - loca)); - } +/** + * Parse contract choices. Upon success, continue + * processing with merge_inventory(). + * + * @param[in,out] oc order context + */ +static void +parse_choices (struct OrderContext *oc) +{ + if (NULL == oc->parse_order.choices) + { + oc->phase++; + return; + } + + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + json_array_size (oc->parse_order.choices)); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + const char *error_name; + unsigned int error_line; + const json_t *jinputs; + const json_t *joutputs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_JSON_parse (json_array_get (oc->parse_order.choices, i), + spec, + &error_name, + &error_line); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Choice parsing failed: %s:%u\n", + error_name, + error_line); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice"); + return; } + + if (! json_is_array (jinputs) || + ! json_is_array (joutputs)) { - json_t *locj; + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inputs or outputs"); + return; + } - /* Handle merchant jurisdiction */ - locj = settings->jurisdiction; - if (NULL != locj) + { + // TODO: Maybe move to a separate function + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) { - locj = json_deep_copy (locj); - GNUNET_assert (0 == - json_object_set_new (jm, - "jurisdiction", - locj)); + // TODO: Assuming this struct would have more fields, would i use GNUNET_new then? + // Or should i use GNUNET_grow first and then get the element using the index? + // Assuming you add a field in the future, it's easier to that way, so you don't + // free it. + struct TALER_MerchantContractInput input = {.details.token.count = 1}; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &input.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &input.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &input.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (jinput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid input #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return; + } + + input.type = TMH_string_to_contract_input_type (kind); + + if (TALER_MCIT_INVALID == input.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in input #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return; + } + + if (0 == input.details.token.count) + { + /* Ignore inputs with 'number' field set to 0 */ + continue; + } + + bool found = false; + + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + if (0 == strcmp (oc->parse_choices.authorities[i].label, + input.details.token.token_family_slug)) + { + found = true; + break; + } + } + + if (! found) + { + MHD_RESULT res; + res = set_token_authority (oc, + input.details.token.token_family_slug, + input.details.token.valid_after); + + if (MHD_NO == res) + { + return; + } + } + + GNUNET_array_append (oc->parse_choices.choices[i].inputs, + oc->parse_choices.choices[i].inputs_len, + input); } } - GNUNET_assert (0 == - json_object_set_new (order, - "merchant", - jm)); - } - /* 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)) - { - GNUNET_JSON_parse_free (spec); - 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"); - } + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MerchantContractOutput output = { .details.token.count = 1 }; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &output.details.token.token_family_slug), + GNUNET_JSON_spec_timestamp ("valid_after", + &output.details.token.valid_after), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &output.details.token.count), + NULL), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (joutput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_JSON_parse_free (spec); + GNUNET_JSON_parse_free (ispec); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid output #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return; + } - 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"); - } + output.type = TMH_string_to_contract_output_type (kind); - /* sanity check result */ - { - struct TALER_PrivateContractHashP h_control; + if (TALER_MCOT_INVALID == output.type) + { + GNUNET_JSON_parse_free (spec); + GNUNET_JSON_parse_free (ispec); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in output #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return; + } - switch (TALER_JSON_contract_hash (order, - &h_control)) - { - case GNUNET_SYSERR: - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - 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_JSON_parse_free (spec); - 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; + if (0 == output.details.token.count) + { + /* Ignore outputs with 'number' field set to 0 */ + continue; + } + + bool found = false; + + for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++) + { + if (0 == strcmp (oc->parse_choices.authorities[i].label, + output.details.token.token_family_slug)) + { + found = true; + break; + } + } + + if (! found) + { + MHD_RESULT res; + res = set_token_authority (oc, + output.details.token.token_family_slug, + output.details.token.valid_after); + + if (MHD_NO == res) + { + return; + } + } + + GNUNET_array_append (oc->parse_choices.choices[i].outputs, + oc->parse_choices.choices[i].outputs_len, + output); + } } } - { - MHD_RESULT mres; - mres = execute_order (connection, - hc, - h_post_data, - order, - claim_token, - inventory_products_length, - inventory_products, - uuids_length, - uuids); - GNUNET_JSON_parse_free (spec); - return mres; - } + oc->phase++; } - /** * 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(). + * processing with set_exchanges(). * - * @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 + * @param[in,out] oc order context */ -static MHD_RESULT -add_payment_details (struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - const struct TALER_MerchantPostDataHashP *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[]) +static void +add_payment_details (struct OrderContext *oc) { struct TMH_WireMethod *wm; - wm = hc->instance->wm_head; + wm = oc->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, + ( (NULL != oc->parse_request.payment_target) && + (0 != strcasecmp (oc->parse_request.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); + oc->hc->instance->settings.id); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, + oc->parse_request.payment_target); + return; } - 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); + oc->add_payment_details.wm = wm; + oc->phase++; } /** - * Merge the inventory products into @a order, querying the + * Merge the inventory products into products, 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 + * @param[in,out] oc order context to process */ -static MHD_RESULT -merge_inventory (struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - const struct TALER_MerchantPostDataHashP *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[]) +static void +merge_inventory (struct OrderContext *oc) { /** - * inventory_products => instructions to add products to contract terms - * order.products => contains products that are not from the backend-managed inventory. + * parse_request.inventory_products => instructions to add products to contract terms + * parse_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 (! TMH_products_array_valid (jprod)) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.products"); - } - } - + if (NULL != oc->parse_order.products) + oc->merge_inventory.products + = json_deep_copy (oc->parse_order.products); + else + oc->merge_inventory.products + = json_array (); /* Populate products from inventory product array and database */ { - json_t *np = json_array (); - - for (unsigned int i = 0; i<inventory_products_length; i++) + GNUNET_assert (NULL != oc->merge_inventory.products); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) { + const struct InventoryProduct *ip + = &oc->parse_request.inventory_products[i]; struct TALER_MERCHANTDB_ProductDetails pd; enum GNUNET_DB_QueryStatus qs; qs = TMH_db->lookup_product (TMH_db->cls, - hc->instance->settings.id, - inventory_products[i].product_id, + oc->hc->instance->settings.id, + ip->product_id, &pd); if (qs <= 0) { @@ -1353,7 +2616,7 @@ merge_inventory (struct MHD_Connection *connection, case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product %s from order unknown\n", - inventory_products[i].product_id); + ip->product_id); http_status = MHD_HTTP_NOT_FOUND; ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; break; @@ -1361,12 +2624,16 @@ merge_inventory (struct MHD_Connection *connection, /* 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_decref (oc->merge_inventory.products); + reply_with_error (oc, + http_status, + ec, + ip->product_id); + return; } + oc->parse_order.minimum_age + = GNUNET_MAX (oc->parse_order.minimum_age, + pd.minimum_age); { json_t *p; @@ -1383,12 +2650,12 @@ merge_inventory (struct MHD_Connection *connection, pd.taxes), GNUNET_JSON_pack_string ("image", pd.image), - GNUNET_JSON_pack_uint64 ("quantity", - inventory_products[i]. - quantity)); + GNUNET_JSON_pack_uint64 ( + "quantity", + ip->quantity)); GNUNET_assert (NULL != p); GNUNET_assert (0 == - json_array_append_new (np, + json_array_append_new (oc->merge_inventory.products, p)); } GNUNET_free (pd.description); @@ -1396,160 +2663,181 @@ merge_inventory (struct MHD_Connection *connection, 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); + /* check if final product list is well-formed */ + if (! TMH_products_array_valid (oc->merge_inventory.products)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order:products"); + return; + } + oc->phase++; } /** - * 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. + * Parse the client request. Upon success, + * continue processing by calling parse_order(). * - * @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 + * @param[in,out] oc order context to process */ -MHD_RESULT -TMH_private_post_orders (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +static void +parse_request (struct OrderContext *oc) { - json_t *order; - struct GNUNET_TIME_Relative refund_delay = GNUNET_TIME_UNIT_ZERO; - 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; + const json_t *ip = NULL; + const json_t *uuid = NULL; + const char *otp_id = NULL; bool create_token = true; /* default */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("order", - &order), + &oc->parse_request.order), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("refund_delay", - &refund_delay), + &oc->parse_request.refund_delay), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("payment_target", - &payment_target), + &oc->parse_request.payment_target), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("inventory_products", - &ip), + GNUNET_JSON_spec_array_const ("inventory_products", + &ip), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("lock_uuids", - &uuid), + GNUNET_JSON_spec_string ("session_id", + &oc->parse_request.session_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("lock_uuids", + &uuid), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_bool ("create_token", &create_token), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("otp_id", + &otp_id), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; - struct TALER_MerchantPostDataHashP h_post_data; - (void) rh; - ret = TALER_MHD_parse_json_data (connection, - hc->request_body, + ret = TALER_MHD_parse_json_data (oc->connection, + oc->hc->request_body, spec); if (GNUNET_OK != ret) - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Refund delay is %s\n", - GNUNET_TIME_relative2s (refund_delay, + GNUNET_TIME_relative2s (oc->parse_request.refund_delay, false)); - TMH_db->expire_locks (TMH_db->cls); - if (create_token) + if (NULL != otp_id) { - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &claim_token, - sizeof (claim_token)); + struct TALER_MERCHANTDB_OtpDeviceDetails td; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->select_otp (TMH_db->cls, + oc->hc->instance->settings.id, + otp_id, + &td); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_otp"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "select_otp"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + otp_id); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + oc->parse_request.pos_key = td.otp_key; + oc->parse_request.pos_algorithm = td.otp_algorithm; } - else + if (create_token) { - /* we use all-zeros for 'no token' */ - memset (&claim_token, - 0, - sizeof (claim_token)); + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &oc->parse_request.claim_token, + sizeof (oc->parse_request.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))) + if (NULL == (req_body_enc + = json_dumps (oc->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"); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "request body normalization for hashing"); + return; } GNUNET_CRYPTO_hash (req_body_enc, strlen (req_body_enc), - &h_post_data.hash); + &oc->parse_request.h_post_data.hash); GNUNET_free (req_body_enc); } /* parse the inventory_products (optionally given) */ if (NULL != ip) { - if (! json_is_array (ip)) + unsigned int ipl = (unsigned int) json_array_size (ip); + + if ( (json_array_size (ip) != (size_t) ipl) || + (ipl > MAX_PRODUCTS) ) { + GNUNET_break (0); GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_products"); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "inventory products too long"); + return; } - GNUNET_array_grow (ips, - ips_len, - json_array_size (ip)); - for (unsigned int i = 0; i<ips_len; i++) + GNUNET_array_grow (oc->parse_request.inventory_products, + oc->parse_request.inventory_products_length, + (unsigned int) json_array_size (ip)); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) { + struct InventoryProduct *ipr = &oc->parse_request.inventory_products[i]; const char *error_name; unsigned int error_line; struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_string ("product_id", - &ips[i].product_id), + &ipr->product_id), GNUNET_JSON_spec_uint32 ("quantity", - &ips[i].quantity), + &ipr->quantity), GNUNET_JSON_spec_end () }; @@ -1561,19 +2849,16 @@ TMH_private_post_orders (const struct TMH_RequestHandler *rh, if (GNUNET_OK != ret) { GNUNET_break_op (0); - GNUNET_array_grow (ips, - ips_len, - 0); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product parsing failed at #%u: %s:%u\n", i, error_name, error_line); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_products"); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_products"); + return; } } } @@ -1581,21 +2866,10 @@ TMH_private_post_orders (const struct TMH_RequestHandler *rh, /* parse the lock_uuids (optionally given) */ if (NULL != uuid) { - if (! json_is_array (uuid)) - { - GNUNET_array_grow (ips, - ips_len, - 0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "lock_uuids"); - } - GNUNET_array_grow (uuids, - uuids_len, + GNUNET_array_grow (oc->parse_request.uuids, + oc->parse_request.uuids_length, json_array_size (uuid)); - for (unsigned int i = 0; i<uuids_len; i++) + for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) { json_t *ui = json_array_get (uuid, i); @@ -1603,49 +2877,89 @@ TMH_private_post_orders (const struct TMH_RequestHandler *rh, if (! json_is_string (ui)) { GNUNET_break_op (0); - GNUNET_array_grow (ips, - ips_len, - 0); - GNUNET_array_grow (uuids, - uuids_len, - 0); - GNUNET_JSON_parse_free (spec); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "UUID parsing failed at #%u\n", i); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "lock_uuids"); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "lock_uuids"); + return; } TMH_uuid_from_string (json_string_value (ui), - &uuids[i]); + &oc->parse_request.uuids[i]); } } + oc->phase++; +} - /* Finally, start by completing the order */ - { - MHD_RESULT res; - res = merge_inventory (connection, - hc, - &h_post_data, - order, - &claim_token, - refund_delay, - payment_target, - ips_len, - ips, - uuids_len, - uuids); - GNUNET_array_grow (ips, - ips_len, - 0); - GNUNET_array_grow (uuids, - uuids_len, - 0); - GNUNET_JSON_parse_free (spec); - return res; +MHD_RESULT +TMH_private_post_orders ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct OrderContext *oc = hc->ctx; + + if (NULL == oc) + { + oc = GNUNET_new (struct OrderContext); + hc->ctx = oc; + hc->cc = &clean_order; + oc->connection = connection; + oc->hc = hc; + } + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing order in phase %d\n", + oc->phase); + switch (oc->phase) + { + case ORDER_PHASE_PARSE_REQUEST: + parse_request (oc); + break; + case ORDER_PHASE_PARSE_ORDER: + parse_order (oc); + break; + case ORDER_PHASE_PARSE_CHOICES: + parse_choices (oc); + break; + case ORDER_PHASE_MERGE_INVENTORY: + merge_inventory (oc); + break; + case ORDER_PHASE_ADD_PAYMENT_DETAILS: + add_payment_details (oc); + break; + case ORDER_PHASE_SET_EXCHANGES: + if (set_exchanges (oc)) + return MHD_YES; + break; + case ORDER_PHASE_SET_MAX_FEE: + set_max_fee (oc); + break; + case ORDER_PHASE_SERIALIZE_ORDER: + serialize_order (oc); + break; + case ORDER_PHASE_CHECK_CONTRACT: + check_contract (oc); + break; + case ORDER_PHASE_SALT_FORGETTABLE: + salt_forgettable (oc); + break; + case ORDER_PHASE_EXECUTE_ORDER: + execute_order (oc); + break; + case ORDER_PHASE_FINISHED_MHD_YES: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished processing order (1)\n"); + return MHD_YES; + case ORDER_PHASE_FINISHED_MHD_NO: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished processing order (0)\n"); + return MHD_NO; + } } } |