/* This file is part of TALER (C) 2014, 2015, 2016 GNUnet e.V. and INRIA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file backend/taler-merchant-httpd_pay.c * @brief handling of /pay requests * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" #include #include #include #include #include #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_parsing.h" #include "taler-merchant-httpd_responses.h" #include "taler-merchant-httpd_auditors.h" #include "taler-merchant-httpd_exchanges.h" /** * How long to wait before giving up processing with the exchange? */ #define PAY_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30)) /** * Information we keep for an individual call to the /pay handler. */ struct PayContext; /** * Information kept during a /pay request for each coin. */ struct DepositConfirmation { /** * Reference to the main PayContext */ struct PayContext *pc; /** * Handle to the deposit operation we are performing for * this coin, NULL after the operation is done. */ struct TALER_EXCHANGE_DepositHandle *dh; /** * Denomination of this coin. */ struct TALER_DenominationPublicKey denom; /** * Amount this coin contributes to the total purchase price. * This amount includes the deposit fee. */ struct TALER_Amount amount_with_fee; /** * Fee charged by the exchange for the deposit operation of this coin. */ struct TALER_Amount deposit_fee; /** * Public key of the coin. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Signature using the @e denom key over the @e coin_pub. */ struct TALER_DenominationSignature ub_sig; /** * Signature of the coin's private key over the contract. */ struct TALER_CoinSpendSignatureP coin_sig; /** * Offset of this coin into the `dc` array of all coins in the * @e pc. */ unsigned int index; /** * #GNUNET_YES if we found this coin in the database. */ int found_in_db; }; /** * Information we keep for an individual call to the /pay handler. */ struct PayContext { /** * This field MUST be first. * FIXME: Explain why! */ struct TM_HandlerContext hc; /** * Array with @e coins_cnt coins we are despositing. */ struct DepositConfirmation *dc; /** * MHD connection to return to */ struct MHD_Connection *connection; /** * Handle to the exchange that we are doing the payment with. * (initially NULL while @e fo is trying to find a exchange). */ struct TALER_EXCHANGE_Handle *mh; /** * Handle for operation to lookup /keys (and auditors) from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_FindOperation *fo; /** * Placeholder for #TMH_PARSE_post_json() to keep its internal state. */ void *json_parse_context; /** * Exchange URI given in @e root. */ char *chosen_exchange; /** * Transaction ID given in @e root. */ const char *order_id; /** * Maximum fee the merchant is willing to pay, from @e root. * Note that IF the total fee of the exchange is higher, that is * acceptable to the merchant if the customer is willing to * pay the difference (i.e. amount - max_fee <= actual-amount - actual-fee). */ struct TALER_Amount max_fee; /** * Amount from @e root. This is the amount the merchant expects * to make, minus @e max_fee. */ struct TALER_Amount amount; /** * Timestamp from @e root. */ struct GNUNET_TIME_Absolute timestamp; /** * Refund deadline from @e root. */ struct GNUNET_TIME_Absolute refund_deadline; /** * Deadline for the customer to pay for this contract. */ struct GNUNET_TIME_Absolute pay_deadline; /** * "H_contract" from @e root. */ struct GNUNET_HashCode h_proposal_data; /** * Wire transfer deadline. How soon would the merchant like the * wire transfer to be executed? (Can be given by the frontend * or be determined by our configuration via #wire_transfer_delay.) */ struct GNUNET_TIME_Absolute wire_transfer_deadline; /** * Response to return, NULL if we don't have one yet. */ struct MHD_Response *response; /** * Number of coins this payment is made of. Length * of the @e dc array. */ unsigned int coins_cnt; /** * Number of transactions still pending. Initially set to * @e coins_cnt, decremented on each transaction that * successfully finished. */ unsigned int pending; /** * HTTP status code to use for the reply, i.e 200 for "OK". * Special value UINT_MAX is used to indicate hard errors * (no reply, return #MHD_NO). */ unsigned int response_code; /** * Task called when the (suspended) processing for * the /pay request times out. * Happens when we don't get a response from the exchange. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * #GNUNET_NO if the transaction is not in our database, * #GNUNET_YES if the transaction is known to our database, * #GNUNET_SYSERR if the transaction ID is used for a different * transaction in our database. */ int transaction_exits; /** * Instance of the payment's instance (in JSON format) */ struct MerchantInstance *mi; /** * Proposal data for the proposal that is being * payed for in this context. */ json_t *proposal_data; }; /** * Resume the given pay context and send the given response. * Stores the response in the @a pc and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param pc payment context * @param response_code response code to use * @param response response data to send back */ static void resume_pay_with_response (struct PayContext *pc, unsigned int response_code, struct MHD_Response *response) { pc->response_code = response_code; pc->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay handling as exchange interaction is done (%u)\n", response_code); if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Convert denomination key to its base32 representation * * @param dk denomination key to convert * @return 0-terminated base32 encoding of @a dk, to be deallocated */ static char * denomination_to_string_alloc (struct TALER_DenominationPublicKey *dk) { char *buf; char *buf2; size_t buf_size; buf_size = GNUNET_CRYPTO_rsa_public_key_encode (dk->rsa_public_key, &buf); buf2 = GNUNET_STRINGS_data_to_string_alloc (buf, buf_size); GNUNET_free (buf); return buf2; } /** * Abort all pending /deposit operations. * * @param pc pay context to abort */ static void abort_deposit (struct PayContext *pc) { unsigned int i; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Aborting pending /deposit operations\n"); for (i=0;icoins_cnt;i++) { struct DepositConfirmation *dci = &pc->dc[i]; if (NULL != dci->dh) { TALER_EXCHANGE_deposit_cancel (dci->dh); dci->dh = NULL; } } } /** * Generate a response that indicates payment success. * * @param pc payment context * @return the mhd response */ struct MHD_Response * sign_success_response (struct PayContext *pc) { struct GNUNET_CRYPTO_EddsaSignature sig; struct PaymentResponsePS mr; mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK); mr.purpose.size = htonl (sizeof (mr)); mr.h_proposal_data = pc->h_proposal_data; GNUNET_CRYPTO_eddsa_sign (&pc->mi->privkey.eddsa_priv, &mr.purpose, &sig); return TMH_RESPONSE_make_json_pack ("{s:O, s:s, s:o}", "proposal_data", pc->proposal_data, "sig", json_string_value (GNUNET_JSON_from_data_auto (&sig)), "h_proposal_data", GNUNET_JSON_from_data (&pc->h_proposal_data, sizeof (struct GNUNET_HashCode))); } /** * Callback to handle a deposit permission's response. * * @param cls a `struct DepositConfirmation` (i.e. a pointer * into the global array of confirmations and an index for this call * in that array). That way, the last executed callback can detect * that no other confirmations are on the way, and can pack a response * for the wallet * @param http_status HTTP response code, #MHD_HTTP_OK * (200) for successful deposit; 0 if the exchange's reply is bogus (fails * to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param sign_key which key did the exchange use to sign the @a proof * @param proof the received JSON reply, * should be kept as proof (and, in case of errors, be forwarded to * the customer) */ static void deposit_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *sign_key, const json_t *proof) { struct DepositConfirmation *dc = cls; struct PayContext *pc = dc->pc; dc->dh = NULL; pc->pending--; if (MHD_HTTP_OK != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Deposit operation failed with HTTP code %u\n", http_status); /* Transaction failed; stop all other ongoing deposits */ abort_deposit (pc); if (NULL == proof) { /* We can't do anything meaningful here, the exchange did something wrong */ resume_pay_with_response (pc, MHD_HTTP_SERVICE_UNAVAILABLE, TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:I, s:s}", "error", "exchange failed", "code", (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, "exchange-code", (json_int_t) ec, "exchange-http-status", (json_int_t) http_status, "hint", "The exchange provided an unexpected response")); } else { /* Forward error, adding the "coin_pub" for which the error was being generated */ json_t *eproof; eproof = json_copy ((json_t *) proof); json_object_set_new (eproof, "coin_pub", GNUNET_JSON_from_data_auto (&dc->coin_pub)); resume_pay_with_response (pc, http_status, TMH_RESPONSE_make_json (eproof)); json_decref (eproof); } return; } /* store result to DB */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Storing successful payment for h_proposal_data '%s'\n", GNUNET_h2s (&pc->h_proposal_data)); if (GNUNET_OK != db->store_deposit (db->cls, &pc->h_proposal_data, &pc->mi->pubkey, &dc->coin_pub, &dc->amount_with_fee, &dc->deposit_fee, sign_key, proof)) { GNUNET_break (0); /* internal error */ abort_deposit (pc); /* Forward error including 'proof' for the body */ resume_pay_with_response (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_DB_STORE_PAY_ERROR, "Merchant database error")); return; } if (0 != pc->pending) return; /* still more to do */ resume_pay_with_response (pc, MHD_HTTP_OK, sign_success_response (pc)); } /** * Custom cleanup routine for a `struct PayContext`. * * @param hc the `struct PayContext` to clean up. */ static void pay_context_cleanup (struct TM_HandlerContext *hc) { struct PayContext *pc = (struct PayContext *) hc; unsigned int i; if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } TMH_PARSE_post_cleanup_callback (pc->json_parse_context); for (i=0;icoins_cnt;i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (NULL != dc->dh) { TALER_EXCHANGE_deposit_cancel (dc->dh); dc->dh = NULL; } if (NULL != dc->denom.rsa_public_key) { GNUNET_CRYPTO_rsa_public_key_free (dc->denom.rsa_public_key); dc->denom.rsa_public_key = NULL; } if (NULL != dc->ub_sig.rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (dc->ub_sig.rsa_signature); dc->ub_sig.rsa_signature = NULL; } } GNUNET_free_non_null (pc->dc); if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } if (NULL != pc->response) { MHD_destroy_response (pc->response); pc->response = NULL; } if (NULL != pc->chosen_exchange) { GNUNET_free (pc->chosen_exchange); pc->chosen_exchange = NULL; } if (NULL != pc->proposal_data) { json_decref (pc->proposal_data); pc->proposal_data = NULL; } GNUNET_free (pc); } /** * Function called with the result of our exchange lookup. * * @param cls the `struct PayContext` * @param mh NULL if exchange was not found to be acceptable * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config */ static void process_pay_with_exchange (void *cls, struct TALER_EXCHANGE_Handle *mh, int exchange_trusted) { struct PayContext *pc = cls; struct TALER_Amount acc_fee; struct TALER_Amount acc_amount; const struct TALER_EXCHANGE_Keys *keys; unsigned int i; pc->fo = NULL; if (NULL == mh) { /* The exchange on offer is not in the set of our (trusted) exchanges. Reject the payment. */ GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_PRECONDITION_FAILED, TMH_RESPONSE_make_external_error (TALER_EC_PAY_EXCHANGE_REJECTED, "exchange not supported")); return; } pc->mh = mh; keys = TALER_EXCHANGE_get_keys (mh); if (NULL == keys) { GNUNET_break (0); resume_pay_with_response (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, "no keys")); return; } /* Total up the fees and the value of the deposited coins! */ for (i=0;icoins_cnt;i++) { struct DepositConfirmation *dc = &pc->dc[i]; const struct TALER_EXCHANGE_DenomPublicKey *denom_details; denom_details = TALER_EXCHANGE_get_denomination_key (keys, &dc->denom); if (NULL == denom_details) { char *denom_enc; GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o, s:o}", "error", "denomination not found", "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND, "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key), "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh))); denom_enc = denomination_to_string_alloc (&dc->denom); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "unknown denom to exchange: %s\n", denom_enc); GNUNET_free (denom_enc); return; } if (GNUNET_OK != TMH_AUDITORS_check_dk (mh, denom_details, exchange_trusted)) { char *denom_enc; GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o}", "error", "invalid denomination", "code", (json_int_t) TALER_EC_PAY_DENOMINATION_KEY_AUDITOR_FAILURE, "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key))); denom_enc = denomination_to_string_alloc (&dc->denom); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Client offered invalid denomination: %s\n", denom_enc); GNUNET_free (denom_enc); return; } dc->deposit_fee = denom_details->fee_deposit; if (0 == i) { acc_fee = denom_details->fee_deposit; acc_amount = dc->amount_with_fee; } else { if ( (GNUNET_OK != TALER_amount_add (&acc_fee, &denom_details->fee_deposit, &acc_fee)) || (GNUNET_OK != TALER_amount_add (&acc_amount, &dc->amount_with_fee, &acc_amount)) ) { GNUNET_break_op (0); /* Overflow in these amounts? Very strange. */ resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts")); return; } } if (1 == TALER_amount_cmp (&dc->deposit_fee, &dc->amount_with_fee)) { GNUNET_break_op (0); /* fee higher than residual coin value, makes no sense. */ resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o, s:o}", "hint", "fee higher than coin value", "code", (json_int_t) TALER_EC_PAY_FEES_EXCEED_PAYMENT, "f" /* FIXME */, TALER_JSON_from_amount (&dc->amount_with_fee), "fee_deposit", TALER_JSON_from_amount (&denom_details->fee_deposit))); return; } } /* Now check that the customer paid enough for the full contract */ if (-1 == TALER_amount_cmp (&pc->max_fee, &acc_fee)) { /* acc_fee > max_fee, customer needs to cover difference */ struct TALER_Amount excess_fee; struct TALER_Amount total_needed; /* compute fee amount to be covered by customer */ GNUNET_assert (GNUNET_OK == TALER_amount_subtract (&excess_fee, &acc_fee, &pc->max_fee)); /* add that to the total */ if (GNUNET_OK != TALER_amount_add (&total_needed, &excess_fee, &pc->amount)) { GNUNET_break (0); resume_pay_with_response (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_AMOUNT_OVERFLOW, "overflow")); return; } /* check if total payment sufficies */ if (-1 == TALER_amount_cmp (&acc_amount, &total_needed)) { GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_METHOD_NOT_ACCEPTABLE, TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES, "insufficient funds (including excessive exchange fees to be covered by customer)")); return; } } else { /* fees are acceptable, we cover them all; let's check the amount */ if (-1 == TALER_amount_cmp (&acc_amount, &pc->amount)) { GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_METHOD_NOT_ACCEPTABLE, TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT, "insufficient funds")); return; } } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Exchange and fee structure OK. Initiating deposit operation for coins\n"); /* Initiate /deposit operation for all coins */ for (i=0;icoins_cnt;i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (GNUNET_YES == dc->found_in_db) continue; GNUNET_assert (NULL != pc->mi->j_wire); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Timing for this payment, wire_deadline: %llu, refund_deadline: %llu\n", (unsigned long long) pc->wire_transfer_deadline.abs_value_us, (unsigned long long) pc->refund_deadline.abs_value_us); dc->dh = TALER_EXCHANGE_deposit (mh, &dc->amount_with_fee, pc->wire_transfer_deadline, pc->mi->j_wire, &pc->h_proposal_data, &dc->coin_pub, &dc->ub_sig, &dc->denom, pc->timestamp, &pc->mi->pubkey, pc->refund_deadline, &dc->coin_sig, &deposit_cb, dc); if (NULL == dc->dh) { /* Signature was invalid. If the exchange was unavailable, * we'd get that information in the callback. */ GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_UNAUTHORIZED, TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:i}", "hint", "Coin signature invalid.", "code", (json_int_t) TALER_EC_PAY_COIN_SIGNATURE_INVALID, "coin_idx", i)); return; } } } /** * Handle a timeout for the processing of the pay request. * * @param cls closure */ static void handle_pay_timeout (void *cls) { struct PayContext *pc = cls; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay with error after timeout\n"); pc->timeout_task = NULL; if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } resume_pay_with_response (pc, MHD_HTTP_SERVICE_UNAVAILABLE, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_EXCHANGE_TIMEOUT, "exchange not reachable")); } /** * Function called with information about a coin that was deposited. * * @param cls closure * @param transaction_id of the contract * @param coin_pub public key of the coin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param exchange_proof proof from exchange that coin was accepted */ static void check_coin_paid (void *cls, const struct GNUNET_HashCode *h_proposal_data, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const json_t *exchange_proof) { struct PayContext *pc = cls; unsigned int i; if (0 != memcmp (&pc->h_proposal_data, h_proposal_data, sizeof (struct GNUNET_HashCode))) { GNUNET_break (0); return; } for (i=0;icoins_cnt;i++) { struct DepositConfirmation *dc = &pc->dc[i]; if ( (0 != memcmp (coin_pub, &dc->coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP))) || (0 != TALER_amount_cmp (amount_with_fee, &dc->amount_with_fee)) ) continue; dc->found_in_db = GNUNET_YES; pc->pending--; } } /** * Check if the existing transaction matches our transaction. * Update `transaction_exits` accordingly. * * @param cls closure with the `struct PayContext` * @param transaction_id of the contract * @param merchant_pub merchant's public key * @param exchange_uri URI of the exchange * @param h_xwire hash of our wire details * @param timestamp time of the confirmation * @param refund refund deadline * @param total_amount total amount we receive for the contract after fees */ static void check_transaction_exists (void *cls, const struct TALER_MerchantPublicKeyP *merchant_pub, const char *exchange_uri, const struct GNUNET_HashCode *h_proposal_data, const struct GNUNET_HashCode *h_xwire, struct GNUNET_TIME_Absolute timestamp, struct GNUNET_TIME_Absolute refund, const struct TALER_Amount *total_amount) { struct PayContext *pc = cls; if ( (0 == memcmp (h_proposal_data, &pc->h_proposal_data, sizeof (struct GNUNET_HashCode))) && (0 == memcmp (h_xwire, &pc->mi->h_wire, sizeof (struct GNUNET_HashCode))) && (timestamp.abs_value_us == pc->timestamp.abs_value_us) && (refund.abs_value_us == pc->refund_deadline.abs_value_us) && (0 == TALER_amount_cmp (total_amount, &pc->amount) ) ) { pc->transaction_exits = GNUNET_YES; } else { GNUNET_break_op (0); pc->transaction_exits = GNUNET_SYSERR; } } extern struct MerchantInstance * get_instance (struct json_t *json); /** * Just a stub used to double-check if a transaction * has been correctly inserted into db. * * @param cls closure * @param transaction_id of the contract * @param merchant's public key * @param exchange_uri URI of the exchange * @param h_contract hash of the contract * @param h_wire hash of our wire details * @param timestamp time of the confirmation * @param refund refund deadline * @param total_amount total amount we receive for the contract after fees */ static void transaction_double_check (void *cls, const struct TALER_MerchantPublicKeyP *merchant_pub, const char *exchange_uri, const struct GNUNET_HashCode *h_proposal_data, const struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute timestamp, struct GNUNET_TIME_Absolute refund, const struct TALER_Amount *total_amount) { return; } /** * Try to parse the pay request into the given pay context. * * Schedules an error response in the connection on failure. * * @return #GNUNET_YES on success */ static int parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *pc) { json_t *coins; json_t *coin; json_t *merchant; unsigned int coins_index; const char *chosen_exchange; const char *order_id; struct GNUNET_HashCode h_oid; struct TALER_MerchantPublicKeyP merchant_pub; int res; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("coins", &coins), GNUNET_JSON_spec_string ("exchange", &chosen_exchange), GNUNET_JSON_spec_string ("order_id", &order_id), GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub), GNUNET_JSON_spec_end() }; res = TMH_PARSE_json_data (connection, root, spec); if (GNUNET_YES != res) { GNUNET_break (0); return res; } GNUNET_CRYPTO_hash (order_id, strlen (order_id), &h_oid); res = db->find_proposal_data (db->cls, &pc->proposal_data, &h_oid, &merchant_pub); if (GNUNET_OK != res) { if (MHD_YES != TMH_RESPONSE_reply_not_found (connection, TALER_EC_PAY_DB_STORE_PAY_ERROR, "Proposal not found")) { GNUNET_break (0); return GNUNET_SYSERR; } return GNUNET_NO; } if (GNUNET_OK != TALER_JSON_hash (pc->proposal_data, &pc->h_proposal_data)) { if (MHD_YES != TMH_RESPONSE_reply_internal_error (connection, TALER_EC_NONE, "Can not hash proposal")) { GNUNET_break (0); return GNUNET_SYSERR; } return GNUNET_NO; } merchant = json_object_get (pc->proposal_data, "merchant"); if (NULL == merchant) { // invalid contract: GNUNET_break (0); if (MHD_YES != TMH_RESPONSE_reply_internal_error (connection, TALER_EC_NONE, "No merchant field in contract")) { GNUNET_break (0); return GNUNET_SYSERR; } return GNUNET_NO; } pc->mi = get_instance (merchant); if (NULL == pc->mi) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Not able to find the specified instance\n"); if (MHD_NO == TMH_RESPONSE_reply_not_found (connection, TALER_EC_PAY_INSTANCE_UNKNOWN, "Unknown instance given")) { GNUNET_break (0); return GNUNET_SYSERR; } return GNUNET_NO; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "/pay: picked instance %s with key %s\n", pc->mi->id, GNUNET_STRINGS_data_to_string_alloc (&pc->mi->pubkey, sizeof (pc->mi->pubkey))); pc->chosen_exchange = GNUNET_strdup (chosen_exchange); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Parsed JSON for /pay.\n"); { struct GNUNET_JSON_Specification espec[] = { GNUNET_JSON_spec_absolute_time ("refund_deadline", &pc->refund_deadline), GNUNET_JSON_spec_absolute_time ("pay_deadline", &pc->pay_deadline), GNUNET_JSON_spec_absolute_time ("timestamp", &pc->timestamp), TALER_JSON_spec_amount ("max_fee", &pc->max_fee), TALER_JSON_spec_amount ("amount", &pc->amount), GNUNET_JSON_spec_end() }; res = TMH_PARSE_json_data (connection, pc->proposal_data, espec); if (GNUNET_YES != res) { GNUNET_JSON_parse_free (spec); GNUNET_break (0); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } pc->wire_transfer_deadline = GNUNET_TIME_absolute_add (pc->timestamp, wire_transfer_delay); if (pc->wire_transfer_deadline.abs_value_us < pc->refund_deadline.abs_value_us) { GNUNET_break (0); GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_external_error (connection, TALER_EC_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, "refund deadline after wire transfer deadline"); } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed timestamps\n"); pc->coins_cnt = json_array_size (coins); if (0 == pc->coins_cnt) { GNUNET_JSON_parse_free (spec); return TMH_RESPONSE_reply_arg_invalid (connection, TALER_EC_PAY_COINS_ARRAY_EMPTY, "coins"); } /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct DepositConfirmation); /* This loop populates the array 'dc' in 'pc' */ json_array_foreach (coins, coins_index, coin) { struct DepositConfirmation *dc = &pc->dc[coins_index]; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_denomination_public_key ("denom_pub", &dc->denom), TALER_JSON_spec_amount ("f" /* FIXME */, &dc->amount_with_fee), GNUNET_JSON_spec_fixed_auto ("coin_pub", &dc->coin_pub), TALER_JSON_spec_denomination_signature ("ub_sig", &dc->ub_sig), GNUNET_JSON_spec_fixed_auto ("coin_sig", &dc->coin_sig), GNUNET_JSON_spec_end() }; res = TMH_PARSE_json_data (connection, coin, spec); if (GNUNET_YES != res) { GNUNET_JSON_parse_free (spec); json_decref (root); GNUNET_break (0); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } { char *s; s = TALER_amount_to_string (&dc->amount_with_fee); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Coin #%i has f %s\n", coins_index, s); GNUNET_free (s); } dc->index = coins_index; dc->pc = pc; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed coins\n"); pc->pending = pc->coins_cnt; GNUNET_JSON_parse_free (spec); return GNUNET_OK; } /** * Process a payment for a proposal. */ static int handler_pay_json (struct MHD_Connection *connection, json_t *root, struct PayContext *pc) { int ret; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "about to parse '/pay' body\n"); ret = parse_pay (connection, root, pc); if (GNUNET_OK != ret) return ret; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed '/pay' body\n"); /* Check if this payment attempt has already succeeded */ if (GNUNET_SYSERR == db->find_payments (db->cls, &pc->h_proposal_data, &pc->mi->pubkey, &check_coin_paid, pc)) { GNUNET_break (0); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, "Merchant database error"); } if (0 == pc->pending) { struct MHD_Response *resp; int ret; /* Payment succeeded in the past; take short cut and accept immediately */ resp = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response (connection, MHD_HTTP_OK, sign_success_response (pc)); MHD_destroy_response (resp); return ret; } /* Check if transaction is already known, if not store it. */ if (GNUNET_SYSERR == db->find_transaction (db->cls, &pc->h_proposal_data, &pc->mi->pubkey, &check_transaction_exists, pc)) { GNUNET_break (0); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR, "Merchant database error"); } if (GNUNET_SYSERR == pc->transaction_exits) { GNUNET_break (0); return TMH_RESPONSE_reply_external_error (connection, TALER_EC_PAY_DB_TRANSACTION_ID_CONFLICT, "Transaction ID reused with different transaction details"); } if (GNUNET_NO == pc->transaction_exits) { struct GNUNET_TIME_Absolute now; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Dealing with new transaction '%s'\n", GNUNET_h2s (&pc->h_proposal_data)); now = GNUNET_TIME_absolute_get (); if (now.abs_value_us > pc->pay_deadline.abs_value_us) { /* Time expired, we don't accept this payment now! */ const char *pd_str; pd_str = GNUNET_STRINGS_absolute_time_to_string (pc->pay_deadline); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Attempt to get coins for expired contract. Deadline: '%s'\n", pd_str); return TMH_RESPONSE_reply_bad_request (connection, TALER_EC_PAY_OFFER_EXPIRED, "The time to pay for this contract has expired."); } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Storing transaction '%s'\n", GNUNET_h2s (&pc->h_proposal_data)); if (GNUNET_OK != db->store_transaction (db->cls, &pc->h_proposal_data, &pc->mi->pubkey, pc->chosen_exchange, &pc->mi->h_wire, pc->timestamp, pc->refund_deadline, &pc->amount)) { GNUNET_break (0); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR, "Merchant database error"); } if (GNUNET_OK != db->find_transaction (db->cls, &pc->h_proposal_data, &pc->mi->pubkey, &transaction_double_check, NULL)) GNUNET_break (0); } MHD_suspend_connection (connection); /* Find the responsible exchange, this may take a while... */ pc->fo = TMH_EXCHANGES_find_exchange (pc->chosen_exchange, &process_pay_with_exchange, pc); /* ... so we suspend connection until the last coin has been ack'd or until we have encountered a hard error. Eventually, we will resume the connection and send back a response using #resume_pay_with_response(). */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending /pay handling while working with the exchange\n"); pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, &handle_pay_timeout, pc); return GNUNET_OK; } /** * Process a payment for a proposal. * Takes data from the given MHD connection. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] connection_cls the connection's closure * (can be updated) * @param upload_data upload data * @param[in,out] upload_data_size number of bytes (left) in @a * upload_data * @return MHD result code */ int MH_handler_pay (struct TMH_RequestHandler *rh, struct MHD_Connection *connection, void **connection_cls, const char *upload_data, size_t *upload_data_size) { struct PayContext *pc; int res; json_t *root; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "In handler for /pay.\n"); if (NULL == *connection_cls) { pc = GNUNET_new (struct PayContext); pc->hc.cc = &pay_context_cleanup; pc->connection = connection; *connection_cls = pc; } else { /* not the first call, recover state */ pc = *connection_cls; } if (0 != pc->response_code) { /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == pc->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } res = MHD_queue_response (connection, pc->response_code, pc->response); if (NULL != pc->response) { MHD_destroy_response (pc->response); pc->response = NULL; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Queueing response (%u) for /pay (%s).\n", (unsigned int) pc->response_code, res ? "OK" : "FAILED"); return res; } if (NULL != pc->chosen_exchange) { // FIXME: explain in comment why this could happen! GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Shouldn't be here. Old MHD version?\n"); return MHD_YES; } res = TMH_PARSE_post_json (connection, &pc->json_parse_context, upload_data, upload_data_size, &root); if (GNUNET_SYSERR == res) { GNUNET_break (0); return TMH_RESPONSE_reply_invalid_json (connection); } if ((GNUNET_NO == res) || (NULL == root)) return MHD_YES; /* the POST's body has to be further fetched */ res = handler_pay_json (connection, root, pc); json_decref (root); if (GNUNET_SYSERR == res) return MHD_NO; return MHD_YES; } /* end of taler-merchant-httpd_pay.c */