/* This file is part of TALER (C) 2014-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_private-post-orders.c * @brief the POST /orders handler * @author Christian Grothoff * @author Marcello Stanisci * @author Christian Blättler */ #include "platform.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_private-post-orders.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" /** * How often do we retry the simple INSERT database transaction? */ #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? */ #define STANDARD_LABEL_MERCHANT_JURISDICTION "_mj" /** * What is the label under which we find/place the merchant's * address in the locations list by default? */ #define STANDARD_LABEL_MERCHANT_ADDRESS "_ma" /** * How long do we wait at most for /keys from the exchange(s)? * Ensures that we do not block forever just because some exchange * fails to respond *or* because our taler-merchant-keyscheck * refuses a forced download. */ #define MAX_KEYS_WAIT \ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) /** * Generate the base URL for the given merchant instance. * * @param connection the MHD connection * @param instance_id the merchant instance ID * @returns the merchant instance's base URL */ static char * make_merchant_base_url (struct MHD_Connection *connection, const char *instance_id) { struct GNUNET_Buffer buf; 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); } /** * Information about a product we are supposed to add to the order * based on what we know it from our inventory. */ struct InventoryProduct { /** * Identifier of the product in the inventory. */ const char *product_id; /** * Number of units of the product to add to the order. */ uint32_t quantity; }; /** * 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 families referenced in the contract. */ struct TALER_MerchantContractTokenFamily *token_families; /** * Length of the @e token_families array. */ unsigned int token_families_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; /** * Maximum amount that could be paid over all * available exchanges. Used to determine if this * order creation requests exceeds legal limits. */ struct TALER_Amount total_exchange_limit; /** * How long do we wait at most until giving up on getting keys? */ struct GNUNET_TIME_Absolute keys_timeout; /** * Task to wake us up on @e keys_timeout. */ struct GNUNET_SCHEDULER_Task *wakeup_task; } 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.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } 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; } for (unsigned int i = 0; iparse_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_choices.choices, oc->parse_choices.choices_len, 0); for (unsigned int i = 0; iparse_choices.token_families_len; i++) { GNUNET_free (oc->parse_choices.token_families[i].name); GNUNET_free (oc->parse_choices.token_families[i].description); json_decref (oc->parse_choices.token_families[i].description_i18n); for (unsigned int j = 0; jparse_choices.token_families[i].keys_len; j++ ) { GNUNET_CRYPTO_blind_sign_pub_decref (oc->parse_choices.token_families[i]. keys[j].pub.public_key); } GNUNET_array_grow (oc->parse_choices.token_families[i].keys, oc->parse_choices.token_families[i].keys_len, 0); } GNUNET_array_grow (oc->parse_choices.token_families, oc->parse_choices.token_families_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[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 OrderContext *oc) { enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Timestamp timestamp; uint64_t order_serial; if (GNUNET_OK != TMH_db->start (TMH_db->cls, "insert_order")) { GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } /* 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, 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) */ TMH_db->rollback (TMH_db->cls); return qs; } /* Migrate locks from UUIDs to new order: first release old locks */ for (unsigned int i = 0; iparse_request.uuids_length; i++) { qs = TMH_db->unlock_inventory (TMH_db->cls, &oc->parse_request.uuids[i]); if (qs < 0) { TMH_db->rollback (TMH_db->cls); return qs; } /* qs == 0 is OK here, that just means we did not HAVE any lock under this UUID */ } /* Migrate locks from UUIDs to new order: acquire new locks (note: this can basically ONLY fail on serializability OR because the UUID locks were insufficient for the desired quantities). */ for (unsigned int i = 0; iparse_request.inventory_products_length; i++) { 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); return qs; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* qs == 0: lock acquisition failed due to insufficient stocks */ TMH_db->rollback (TMH_db->cls); oc->execute_order.out_of_stock_index = i; /* indicate which product is causing the issue */ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } } 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, oc->hc->instance->settings.id, oc->parse_order.order_id, ×tamp, &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { TMH_db->rollback (TMH_db->cls); return qs; } { enum GNUNET_DB_QueryStatus qs; json_t *jhook; jhook = GNUNET_JSON_PACK( GNUNET_JSON_pack_string("order_id", oc->parse_order.order_id), GNUNET_JSON_pack_object_incref("contract", oc->serialize_order.contract), GNUNET_JSON_pack_string("instance_id", oc->hc->instance->settings.id) ); GNUNET_assert(NULL != jhook); qs = TMH_trigger_webhook(oc->hc->instance->settings.id, "order_created", jhook); json_decref(jhook); if (0 < qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); reply_with_error (oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "failed to trigger webhooks"); return qs; } } TMH_notify_order_change (oc->hc->instance, TMH_OSF_NONE, timestamp, order_serial); /* finally, commit transaction (note: if it fails, we ALSO re-acquire the UUID locks, which is exactly what we want) */ qs = TMH_db->commit (TMH_db->cls); if (0 > qs) return qs; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */ } /** * Transform an order into a proposal and store it in the * database. Write the resulting proposal or an error message * of a MHD connection. * * @param[in,out] oc order context */ static void execute_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Executing database transaction to create order '%s' for instance '%s'\n", oc->parse_order.order_id, settings->id); for (unsigned int i = 0; ipreflight (TMH_db->cls); qs = execute_transaction (oc); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } if (0 >= qs) { /* Special report if retries insufficient */ if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_break (0); 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 */ reply_with_error ( oc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, oc->parse_order.order_id); return; } /* Other hard transaction error (disk full, etc.) */ GNUNET_break (0); 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 (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; size_t num_categories = 0; uint64_t *categories = NULL; 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, oc->hc->instance->settings.id, ip->product_id, &pd, &num_categories, &categories); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: GNUNET_free (categories); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order creation failed: product out of stock\n"); ret = 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", pd.total_stock - pd.total_sold - pd.total_lost), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_timestamp ( "restock_expected", pd.next_restock))); TALER_MERCHANTDB_product_details_free (&pd); finalize_order (oc, ret); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 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); reply_with_error ( oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); return; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); reply_with_error ( oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, NULL); return; } GNUNET_break (0); 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 ( 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->parse_request.claim_token) ? NULL : &oc->parse_request.claim_token, sizeof (oc->parse_request.claim_token)))); finalize_order (oc, ret); } } /** * Check that the contract is now well-formed. Upon success, continue * processing with execute_order(). * * @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++; } /** * Get rounded time interval. @a start is calculated by rounding * @a ts down to the nearest multiple of @a precision. @a end is * the next higher multiple of @a precision. * * @param precision rounding precision. * year, month, day, hour, minute are supported. * @param ts timestamp to round * @param[out] start start of the interval * @param[out] end end of the interval * @return #GNUNET_OK on success, #GNUNET_SYSERR on error */ static enum GNUNET_GenericReturnValue get_rounded_time_interval (struct GNUNET_TIME_Relative precision, struct GNUNET_TIME_Timestamp ts, struct GNUNET_TIME_Timestamp *start, struct GNUNET_TIME_Timestamp *end) { struct tm*timeinfo; time_t seconds; seconds = GNUNET_TIME_timestamp_to_s (ts); timeinfo = localtime (&seconds); if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, ==, precision)) { timeinfo->tm_mon = 0; timeinfo->tm_mday = 1; timeinfo->tm_hour = 0; timeinfo->tm_min = 0; timeinfo->tm_sec = 0; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, ==, precision)) { timeinfo->tm_mday = 1; timeinfo->tm_hour = 0; timeinfo->tm_min = 0; timeinfo->tm_sec = 0; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, ==, precision)) { timeinfo->tm_hour = 0; timeinfo->tm_min = 0; timeinfo->tm_sec = 0; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, ==, precision)) { timeinfo->tm_min = 0; timeinfo->tm_sec = 0; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, ==, precision)) { timeinfo->tm_sec = 0; } else { return GNUNET_SYSERR; } *start = GNUNET_TIME_timestamp_from_s (mktime (timeinfo)); if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, ==, precision)) { timeinfo->tm_year++; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, ==, precision)) { timeinfo->tm_mon = (timeinfo->tm_mon + 1) % 12; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, ==, precision)) { timeinfo->tm_mday++; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, ==, precision)) { timeinfo->tm_hour++; } else if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, ==, precision)) { timeinfo->tm_min++; } else { return GNUNET_SYSERR; } *end = GNUNET_TIME_timestamp_from_s (mktime (timeinfo)); return GNUNET_OK; } /** * Check if the token family with the given @a slug is already present in * the list of token families for this order. If not, fetch its details and * add it to the list. Then check if there is a public key with a matching * @a valid_after field. If not, generate a new key pair and store it in the * database. * * @param[in,out] oc order context * @param slug slug of the token family * @param[in,out] valid_after validity start date of the token, subject to rounding. Set to the rounded validity start date of the matching key. * @return #GNUNET_OK on success, #GNUNET_SYSERR on error */ static enum GNUNET_GenericReturnValue set_token_family (struct OrderContext *oc, const char *slug, struct GNUNET_TIME_Timestamp *valid_after) { struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; struct TALER_MerchantContractTokenFamily *family = NULL; enum GNUNET_DB_QueryStatus qs; /* TODO: Implement rounding duration of token family and use this here. */ struct GNUNET_TIME_Relative precision = GNUNET_TIME_UNIT_MONTHS; struct GNUNET_TIME_Timestamp min_valid_after; struct GNUNET_TIME_Timestamp max_valid_after; if (GNUNET_OK != get_rounded_time_interval (precision, *valid_after, &min_valid_after, &max_valid_after)) { GNUNET_break (0); reply_with_error (oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "valid_after"); return GNUNET_SYSERR; } for (unsigned int i = 0; iparse_choices.token_families_len; i++) { if (0 == strcmp (oc->parse_choices.token_families[i].slug, slug)) { family = &oc->parse_choices.token_families[i]; break; } } if (NULL != family) { for (unsigned int i = 0; ikeys_len; i++) { if (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, >=, min_valid_after) && GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, <, max_valid_after)) { /* The token family and a matching key is already added. */ *valid_after = family->keys[i].valid_after; return GNUNET_OK; } } } qs = TMH_db->lookup_token_family_key (TMH_db->cls, oc->hc->instance->settings.id, slug, min_valid_after, max_valid_after, &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 GNUNET_SYSERR; } { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); /* Verify that the token family is valid right now. */ if (GNUNET_TIME_timestamp_cmp (key_details.token_family.valid_after, >, now) || GNUNET_TIME_timestamp_cmp (key_details.token_family.valid_before, <=, now) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Token family expired or not yet valid\n"); reply_with_error (oc, /* TODO: HTTP Status Code GONE would be more elegant, but that is already used to indicate that a product is out of stock. */ MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID, key_details.token_family.slug); return GNUNET_SYSERR; } } /* slug is not needed */ GNUNET_free (key_details.token_family.slug); { struct TALER_MerchantContractTokenFamilyKey key; if (NULL == family) { struct TALER_MerchantContractTokenFamily new_family = { .slug = slug, .name = key_details.token_family.name, .description = key_details.token_family.description, .description_i18n = key_details.token_family.description_i18n, .keys = GNUNET_new (struct TALER_MerchantContractTokenFamilyKey), .keys_len = 0, }; switch (key_details.token_family.kind) { case TALER_MERCHANTDB_TFK_Subscription: new_family.kind = TALER_MCTK_SUBSCRIPTION; new_family.critical = true; // TODO: Set trusted domains break; case TALER_MERCHANTDB_TFK_Discount: new_family.kind = TALER_MCTK_DISCOUNT; new_family.critical = false; // TODO: Set expected domains break; } GNUNET_array_append (oc->parse_choices.token_families, oc->parse_choices.token_families_len, new_family); family = &oc->parse_choices.token_families[oc->parse_choices. token_families_len - 1]; } if (NULL == key_details.pub.public_key) { /* There is no matching key for this token family yet. */ /* We have to generate a fresh key pair. */ /* If public key is NULL, private key must also be NULL */ enum GNUNET_DB_QueryStatus iqs; struct GNUNET_CRYPTO_BlindSignPrivateKey *priv; struct GNUNET_CRYPTO_BlindSignPublicKey *pub; struct GNUNET_TIME_Timestamp valid_before = GNUNET_TIME_absolute_to_timestamp ( GNUNET_TIME_absolute_add (min_valid_after.abs_time, key_details.token_family.duration)); GNUNET_assert (NULL == key_details.priv.private_key); if (GNUNET_TIME_timestamp_cmp (min_valid_after, <, key_details.token_family.valid_after)) { /* If key would start before validity of token family, set valid_after of key to the value of the token family. */ min_valid_after = key_details.token_family.valid_after; } if (GNUNET_TIME_timestamp_cmp (valid_before, >, key_details.token_family.valid_before)) { /* If key would end after validity of token family, set valid_before of key to the value of the token family. */ valid_before = key_details.token_family.valid_before; } GNUNET_CRYPTO_blind_sign_keys_create (&priv, &pub, /* TODO: Make cipher and key length configurable */ GNUNET_CRYPTO_BSA_RSA, 4096); { struct TALER_TokenIssuePublicKeyP token_pub = { .public_key = pub, }; struct TALER_TokenIssuePrivateKeyP token_priv = { .private_key = priv, }; iqs = TMH_db->insert_token_family_key (TMH_db->cls, slug, &token_pub, &token_priv, min_valid_after, valid_before); GNUNET_CRYPTO_blind_sign_priv_decref (priv); if (iqs <= 0) { enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; unsigned int http_status = 0; switch (iqs) { case GNUNET_DB_STATUS_HARD_ERROR: http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_STORE_FAILED; break; case GNUNET_DB_STATUS_SOFT_ERROR: case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; 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 GNUNET_SYSERR; } key.pub = token_pub; key.valid_after = min_valid_after; key.valid_before = valid_before; } } else { key.pub = key_details.pub; key.valid_after = key_details.valid_after; key.valid_before = key_details.valid_before; } GNUNET_array_append (family->keys, family->keys_len, key); *valid_after = key.valid_after; } return GNUNET_OK; } /** * 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 = &oc->hc->instance->settings; json_t *merchant; json_t *token_families = 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; iparse_choices.token_families_len; i++) { json_t *keys = json_array (); struct TALER_MerchantContractTokenFamily *family = &oc->parse_choices.token_families[i]; json_t *jfamily; for (unsigned int j = 0; jkeys_len; j++) { struct TALER_MerchantContractTokenFamilyKey key = family->keys[j]; json_t *jkey = GNUNET_JSON_PACK ( /* TODO: Remove h_pub. */ GNUNET_JSON_pack_data_auto ("h_pub", &key.pub.public_key->pub_key_hash), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_rsa_public_key ("rsa_pub", key.pub.public_key->details. rsa_public_key)), // GNUNET_JSON_pack_allow_null( // GNUNET_JSON_pack_data_auto ("cs_pub", // &key.pub.public_key->details.cs_public_key)), GNUNET_JSON_pack_int64 ("cipher", key.pub.public_key->cipher), GNUNET_JSON_pack_timestamp ("valid_after", key.valid_after), GNUNET_JSON_pack_timestamp ("valid_before", key.valid_before) ); GNUNET_assert (0 == json_array_append_new (keys, jkey)); } /* TODO: Add 'details' field. */ jfamily = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", family->name), GNUNET_JSON_pack_string ("description", family->description), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("description_i18n", family->description_i18n)), GNUNET_JSON_pack_array_steal ("keys", keys), GNUNET_JSON_pack_bool ("critical", family->critical) ); GNUNET_assert (0 == json_object_set_new (token_families, family->slug, jfamily)); } for (unsigned int i = 0; iparse_choices.choices_len; i++) { const struct TALER_MerchantContractChoice *choice = &oc->parse_choices.choices[i]; json_t *inputs = json_array (); json_t *outputs = json_array (); GNUNET_assert (NULL != inputs); GNUNET_assert (NULL != outputs); for (unsigned int j = 0; jinputs_len; j++) { const struct TALER_MerchantContractInput *input = &choice->inputs[j]; json_t *jinput; /* For now, only tokens are supported */ GNUNET_assert (TALER_MCIT_TOKEN == input->type); jinput = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("kind", TMH_string_from_contract_input_type (input-> type)), GNUNET_JSON_pack_string ("token_family_slug", input->details.token.token_family_slug), GNUNET_JSON_pack_int64 ("number", input->details.token.count), GNUNET_JSON_pack_timestamp ("valid_after", input->details.token.valid_after) ); GNUNET_assert (0 == json_array_append_new (inputs, jinput)); } for (unsigned int j = 0; joutputs_len; j++) { struct TALER_MerchantContractOutput *output = &choice->outputs[j]; json_t *joutput; /* For now, only tokens are supported */ GNUNET_assert (TALER_MCOT_TOKEN == output->type); joutput = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("kind", TMH_string_from_contract_output_type (output-> type)), GNUNET_JSON_pack_string ("token_family_slug", output->details.token.token_family_slug), GNUNET_JSON_pack_int64 ("number", output->details.token.count), GNUNET_JSON_pack_timestamp ("valid_after", output->details.token.valid_after) ); GNUNET_assert (0 == json_array_append_new (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_new (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_steal ("choices", choices) ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_steal ("token_families", token_families) ), 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++; } /** * 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 */ } /** * 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; struct TALER_Amount max_amount; max_amount = oc->parse_order.brutto; res = TMH_exchange_check_debit ( oc->hc->instance->settings.id, exchange, oc->add_payment_details.wm, &max_amount); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s evaluated at %d with max %s\n", url, res, TALER_amount2s (&max_amount)); if ( (! TALER_amount_is_zero (&max_amount)) && (TALER_amount_is_zero (&max_amount)) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", url); return; } 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; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is %s, adding it!\n", url, TALER_amount2s (&max_amount)); GNUNET_assert (0 <= TALER_amount_add ( &oc->set_exchanges.total_exchange_limit, &oc->set_exchanges.total_exchange_limit, &max_amount)); GNUNET_assert (GNUNET_OK == TALER_amount_min (&oc->set_exchanges.total_exchange_limit, &oc->set_exchanges.total_exchange_limit, &oc->parse_order.brutto)); j_exchange = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("url", url), GNUNET_JSON_pack_uint64 ("priority", priority), TALER_JSON_pack_amount ("max_contribution", &max_amount), 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)); } /** * 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); } /** * Check our KYC status at all exchanges as our current limit is * too low and we failed to create an order. * * @param oc order context */ static void notify_kyc_required (const struct OrderContext *oc) { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) }; char *hws; char *extra; json_t *exchange; size_t i; hws = GNUNET_STRINGS_data_to_string_alloc ( &oc->add_payment_details.wm->h_wire, sizeof (oc->add_payment_details.wm->h_wire)); json_array_foreach (oc->set_exchanges.exchanges, i, exchange) { const char *exchange_url = json_string_value (json_object_get (exchange, "url")); GNUNET_asprintf (&extra, "%s %s", hws, exchange_url); TMH_db->event_notify (TMH_db->cls, &es, extra, strlen (extra) + 1); GNUNET_free (extra); } GNUNET_free (hws); } /** * Task run when we are timing out on /keys and will just * proceed with what we got. * * @param cls our `struct OrderContext *` to resume */ static void wakeup_timeout (void *cls) { struct OrderContext *oc = cls; oc->set_exchanges.wakeup_task = NULL; GNUNET_assert (GNUNET_YES == oc->suspended); GNUNET_CONTAINER_DLL_remove (oc_head, oc_tail, oc); MHD_resume_connection (oc->connection); oc->suspended = GNUNET_NO; TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } /** * 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) { if (NULL != oc->set_exchanges.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } if (TALER_amount_is_zero (&oc->parse_order.brutto)) { /* Total amount is zero, so we don't actually need exchanges! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order total is zero, no need for exchanges\n"); oc->set_exchanges.exchanges = json_array (); GNUNET_assert (NULL != oc->set_exchanges.exchanges); oc->phase++; return false; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order total is %s, trying to find exchanges\n", TALER_amount2s (&oc->parse_order.brutto)); /* Note: re-building 'oc->set_exchanges.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.keys_timeout = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); oc->set_exchanges.exchanges = json_array (); GNUNET_assert (NULL != oc->set_exchanges.exchanges); GNUNET_assert ( GNUNET_OK == TALER_amount_set_zero (oc->parse_order.brutto.currency, &oc->set_exchanges.total_exchange_limit)); 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 (GNUNET_TIME_absolute_is_past (oc->set_exchanges.keys_timeout)) { 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.pending_reload_head) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Still trying to (re)load %skeys\n", oc->set_exchanges.pending_reload_head->url); oc->set_exchanges.wakeup_task = GNUNET_SCHEDULER_add_at (oc->set_exchanges.keys_timeout, &wakeup_timeout, oc); 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 (1 == TALER_amount_cmp (&oc->parse_order.brutto, &oc->set_exchanges.total_exchange_limit)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Cannot create order: %s is the sum of hard limits from supported exchanges\n", TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); notify_kyc_required (oc); reply_with_error ( oc, MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); return false; } if (! oc->set_exchanges.exchange_good) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Creating order, but possibly without usable trusted exchanges\n"); } else { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Can create order: %s is the sum of hard limits from supported exchanges\n", TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); } 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; uint64_t version = 0; const json_t *jmerchant = NULL; /* auto_refund only needs to be type-checked, * mostly because in GNUnet relative times can't * be negative. */ bool no_fee; /* TODO: Move "amount" field to choices. This entails moving the stefan-base fee calculation to the parse_choices phase. */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint64 ("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 ("summary_i18n", &oc->parse_order.summary_i18n), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("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", &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", &oc->parse_order.timestamp), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("refund_deadline", &oc->parse_order.refund_deadline), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("pay_deadline", &oc->parse_order.pay_deadline), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", &oc->parse_order.wire_deadline), NULL), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("max_fee", &oc->parse_order.max_fee), &no_fee), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("delivery_location", &oc->parse_order.delivery_location), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_timestamp ("delivery_date", &oc->parse_order.delivery_date), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("minimum_age", &oc->parse_order.minimum_age), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("auto_refund", &oc->parse_order.auto_refund), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_object_const ("extra", &oc->parse_order.extra), NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; 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); finalize_order2 (oc, ret); return; } if (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 (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 == oc->parse_order.order_id) { char buf[256]; time_t timer; struct tm *tm_info; size_t off; uint64_t rand; char *last; time (&timer); tm_info = localtime (&timer); if (NULL == tm_info) { GNUNET_JSON_parse_free (spec); 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, "%Y.%j", tm_info); /* Check for error state of strftime */ GNUNET_assert (0 != off); buf[off++] = '-'; rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, UINT64_MAX); last = GNUNET_STRINGS_data_to_string (&rand, sizeof (uint64_t), &buf[off], sizeof (buf) - off); GNUNET_assert (NULL != last); *last = '\0'; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Assigning order ID `%s' server-side\n", buf); 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 != oc->parse_order.fulfillment_url) { const char *pos; pos = strstr (oc->parse_order.fulfillment_url, "${ORDER_ID}"); if (NULL != pos) { /* replace ${ORDER_ID} with the real order_id */ char *nurl; /* We only allow one placeholder */ if (strstr (pos + strlen ("${ORDER_ID}"), "${ORDER_ID}")) { GNUNET_break_op (0); 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 - oc->parse_order.fulfillment_url), oc->parse_order.fulfillment_url, /* replace ${ORDER_ID} with the right order_id */ oc->parse_order.order_id, /* append rest of original URL */ pos + strlen ("${ORDER_ID}")); oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); GNUNET_free (nurl); } } /* Check soundness of refund deadline, and that a timestamp * is actually present. */ { 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 (oc->parse_order.timestamp.abs_time)) { oc->parse_order.timestamp = now; } /* If no refund_deadline given, set one based on refund_delay. */ if (GNUNET_TIME_absolute_is_never ( oc->parse_order.refund_deadline.abs_time)) { 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"); oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; } else { oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp ( oc->parse_request.refund_delay); } } 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); 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 (oc->parse_order.pay_deadline.abs_time)) { 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; } 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 (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, 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); reply_with_error ( oc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, "order:wire_transfer_deadline"); return; } } if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, <, oc->parse_order.refund_deadline)) { GNUNET_break_op (0); 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; } if (NULL != merchant_base_url) { 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); } else { char *url; url = make_merchant_base_url (oc->connection, settings->id); 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; } if ( (NULL != oc->parse_order.products) && (! TMH_products_array_valid (oc->parse_order.products)) ) { GNUNET_break_op (0); reply_with_error ( oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "order.products"); return; } /* Merchant information must not already be present */ if (NULL != jmerchant) { GNUNET_break_op (0); 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)) ) { GNUNET_break_op (0); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "delivery_location"); return; } oc->phase++; } /** * 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; iparse_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_mark_optional ( GNUNET_JSON_spec_array_const ("inputs", &jinputs), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("outputs", &joutputs), NULL), 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 (0 == json_array_size (jinputs) && 0 == json_array_size (joutputs)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Choice #%u must have at least one input or output\n", i); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "choice"); return; } { // TODO: Maybe move to a separate function const json_t *jinput; size_t idx; json_array_foreach ((json_t *) jinputs, idx, jinput) { 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), /* TODO: Remove valid_after field for inputs, use current system time instead. */ 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_contract_input_type_from_string (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; } if (GNUNET_OK != set_token_family (oc, input.details.token.token_family_slug , &input.details.token.valid_after)) { /* error is already scheduled, return. */ return; } GNUNET_array_append (oc->parse_choices.choices[i].inputs, oc->parse_choices.choices[i].inputs_len, input); } } { 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), /* TODO: Make valid_after optional, default to current system time. */ 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; } output.type = TMH_contract_output_type_from_string (kind); 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; } if (0 == output.details.token.count) { /* Ignore outputs with 'number' field set to 0. */ continue; } if (GNUNET_OK != set_token_family (oc, output.details.token. token_family_slug, &output.details.token.valid_after)) { /* Error is already scheduled, return. */ return; } GNUNET_array_append (oc->parse_choices.choices[i].outputs, oc->parse_choices.choices[i].outputs_len, output); } } } 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 set_exchanges(). * * @param[in,out] oc order context */ static void add_payment_details (struct OrderContext *oc) { struct TMH_WireMethod *wm; wm = oc->hc->instance->wm_head; /* Locate wire method that has a matching payment target */ while ( (NULL != wm) && ( (! wm->active) || ( (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", 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; } oc->add_payment_details.wm = wm; oc->phase++; } /** * 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[in,out] oc order context to process */ static void merge_inventory (struct OrderContext *oc) { /** * parse_request.inventory_products => instructions to add products to contract terms * parse_order.products => contains products that are not from the backend-managed inventory. */ 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 */ { GNUNET_assert (NULL != oc->merge_inventory.products); for (unsigned int i = 0; iparse_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; size_t num_categories = 0; uint64_t *categories = NULL; qs = TMH_db->lookup_product (TMH_db->cls, oc->hc->instance->settings.id, ip->product_id, &pd, &num_categories, &categories); if (qs <= 0) { enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; unsigned int http_status = 0; switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_FETCH_FAILED; break; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; break; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product %s from order unknown\n", ip->product_id); http_status = MHD_HTTP_NOT_FOUND; ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: /* case listed to make compilers happy */ GNUNET_assert (0); } reply_with_error (oc, http_status, ec, ip->product_id); return; } GNUNET_free (categories); oc->parse_order.minimum_age = GNUNET_MAX (oc->parse_order.minimum_age, pd.minimum_age); { json_t *p; p = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("description", pd.description), GNUNET_JSON_pack_object_steal ("description_i18n", pd.description_i18n), GNUNET_JSON_pack_string ("unit", pd.unit), TALER_JSON_pack_amount ("price", &pd.price), GNUNET_JSON_pack_array_steal ("taxes", pd.taxes), GNUNET_JSON_pack_string ("image", pd.image), GNUNET_JSON_pack_uint64 ( "quantity", ip->quantity)); GNUNET_assert (NULL != p); GNUNET_assert (0 == json_array_append_new (oc->merge_inventory.products, p)); } GNUNET_free (pd.description); GNUNET_free (pd.unit); GNUNET_free (pd.image); json_decref (pd.address); } } /* 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++; } /** * Parse the client request. Upon success, * continue processing by calling parse_order(). * * @param[in,out] oc order context to process */ static void parse_request (struct OrderContext *oc) { 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", &oc->parse_request.order), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_relative_time ("refund_delay", &oc->parse_request.refund_delay), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("payment_target", &oc->parse_request.payment_target), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("inventory_products", &ip), NULL), GNUNET_JSON_spec_mark_optional ( 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; ret = TALER_MHD_parse_json_data (oc->connection, oc->hc->request_body, spec); if (GNUNET_OK != ret) { GNUNET_break_op (0); finalize_order2 (oc, ret); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Refund delay is %s\n", GNUNET_TIME_relative2s (oc->parse_request.refund_delay, false)); TMH_db->expire_locks (TMH_db->cls); if (NULL != otp_id) { 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; } if (create_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 (oc->hc->request_body, JSON_ENCODE_ANY | JSON_COMPACT | JSON_SORT_KEYS))) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); 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), &oc->parse_request.h_post_data.hash); GNUNET_free (req_body_enc); } /* parse the inventory_products (optionally given) */ if (NULL != 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); reply_with_error (oc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_ALLOCATION_FAILURE, "inventory products too long"); return; } 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; iparse_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", &ipr->product_id), GNUNET_JSON_spec_uint32 ("quantity", &ipr->quantity), GNUNET_JSON_spec_end () }; ret = GNUNET_JSON_parse (json_array_get (ip, i), ispec, &error_name, &error_line); if (GNUNET_OK != ret) { GNUNET_break_op (0); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Product parsing failed at #%u: %s:%u\n", i, error_name, error_line); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "inventory_products"); return; } } } /* parse the lock_uuids (optionally given) */ if (NULL != uuid) { GNUNET_array_grow (oc->parse_request.uuids, oc->parse_request.uuids_length, json_array_size (uuid)); for (unsigned int i = 0; iparse_request.uuids_length; i++) { json_t *ui = json_array_get (uuid, i); if (! json_is_string (ui)) { GNUNET_break_op (0); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "UUID parsing failed at #%u\n", i); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "lock_uuids"); return; } TMH_uuid_from_string (json_string_value (ui), &oc->parse_request.uuids[i]); } } oc->phase++; } 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; } } } /* end of taler-merchant-httpd_private-post-orders.c */