merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit a0238028dd8ff2f0098465c510460c13b542861d
parent 48a0a077162d7e42c249929eda536fded1b7b940
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 19 May 2016 18:26:00 +0200

Merge branch 'master' of git.taler.net:/var/git/merchant

Diffstat:
M.gitignore | 1+
Msrc/backend/merchant.conf | 16+++++-----------
Msrc/backend/taler-merchant-httpd.c | 4++++
Msrc/backend/taler-merchant-httpd_auditors.c | 4++--
Msrc/backend/taler-merchant-httpd_contract.c | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/backend/taler-merchant-httpd_exchanges.c | 4++--
Msrc/backend/taler-merchant-httpd_pay.c | 48++++++++++++++++++++++++++++++++++++++++++++----
Msrc/backend/taler-merchant-httpd_pay.h | 1-
Msrc/backend/taler-merchant-httpd_util.c | 5+----
Asrc/backend/test-merchant.conf | 0
Msrc/backenddb/plugin_merchantdb_postgres.c | 21+++++++++++++++++++++
Msrc/include/taler_merchant_service.h | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/include/taler_merchantdb_plugin.h | 9+++++++++
Msrc/lib/Makefile.am | 4++++
Asrc/lib/merchant_api_contract.c | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_pay.c | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/lib/test_merchant_api.c | 357++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/lib/test_merchant_api.conf | 15+++++++++++++--
18 files changed, 898 insertions(+), 186 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -27,5 +27,6 @@ GTAGS *.swp src/backend/taler-merchant-httpd src/lib/test_merchant_api +src/lib/test_merchant_api_home/.local/share/taler/exchange/live-keys/ taler_merchant_config.h taler_merchant_config.h.in diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -21,7 +21,8 @@ DB = postgres # Which wireformat does this merchant use? (test/sepa/etc.) # WIREFORMAT = "test" -# Must match the specification given in [merchant-wireformat] +# Determines which wire plugin will be used. We currently only +# support one wire plugin at a time! # Configuration for postgres database. @@ -31,14 +32,7 @@ CONFIG = postgres:///talermerchant # Configuration of our bank account details [merchant-wireformat] +# Default location for the 'test' wire plugin TEST_RESPONSE_FILE = ${TALER_CONFIG_HOME}/merchant/wire/test.json -# The values in this section must match the "WIREFORMAT" given in [merchant]: -# * for SEPA: -# IBAN = DE67830654080004822650 -# NAME = GNUNET E.V -# BIC = GENODEF1SRL -# -# * for TEST: -# ACCOUNT_NUMBER = 123456 -# BANK_URI = http:// -# +# Default location for the 'sepa' wire plugin +SEPA_RESPONSE_FILE = ${TALER_CONFIG_HOME}/merchant/wire/sepa.json diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -553,6 +553,10 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + if (GNUNET_YES != GNUNET_DISK_file_test (keyfile)) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Merchant private key `%s' does not exist yet, creating it!\n", + keyfile); if (NULL == (privkey = GNUNET_CRYPTO_eddsa_key_create_from_file (keyfile))) diff --git a/src/backend/taler-merchant-httpd_auditors.c b/src/backend/taler-merchant-httpd_auditors.c @@ -128,8 +128,8 @@ parse_auditors (void *cls, struct Auditor auditor; if (0 != strncasecmp (section, - "auditor-", - strlen ("auditor-"))) + "merchant-auditor-", + strlen ("merchant-auditor-"))) return; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c @@ -31,6 +31,58 @@ extern char *TMH_merchant_currency_string; + +/** + * Check that the given JSON array of products is well-formed. + * + * @param products JSON array to check + * @return #GNUNET_OK if all is fine + */ +static int +check_products (json_t *products) +{ + size_t index; + json_t *value; + int res; + + if (! json_is_array (products)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + json_array_foreach (products, index, value) { + const char *description; + const char *error_name; + unsigned int error_line; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("description", &description), + /* FIXME: there are other fields in the product specification + that rre currently not labeled as optional. Maybe check + those as well, or make them truly optional. */ + GNUNET_JSON_spec_end() + }; + + /* extract fields we need to sign separately */ + res = GNUNET_JSON_parse (value, + spec, + &error_name, + &error_line); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Product description parsing failed at #%u: %s:%u\n", + (unsigned int) index, + error_name, + error_line); + return GNUNET_SYSERR; + } + GNUNET_JSON_parse_free (spec); + } + return GNUNET_OK; +} + + /** * Manage a contract request. In practical terms, it adds the fields * 'exchanges', 'merchant_pub', and 'H_wire' to the contract 'proposition' @@ -64,10 +116,20 @@ MH_handler_contract (struct TMH_RequestHandler *rh, struct TALER_Amount total; struct TALER_Amount max_fee; uint64_t transaction_id; + json_t *products; + struct GNUNET_TIME_Absolute timestamp; + struct GNUNET_TIME_Absolute refund_deadline; + struct GNUNET_TIME_Absolute expiry; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount ("amount", &total), TALER_JSON_spec_amount ("max_fee", &max_fee), GNUNET_JSON_spec_uint64 ("transaction_id", &transaction_id), + /* The following entries we don't actually need, except to check that + the contract is well-formed */ + GNUNET_JSON_spec_json ("products", &products), + GNUNET_JSON_spec_absolute_time ("timestamp", &timestamp), + GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), + GNUNET_JSON_spec_absolute_time ("expiry", &expiry), GNUNET_JSON_spec_end() }; @@ -97,6 +159,7 @@ MH_handler_contract (struct TMH_RequestHandler *rh, if (NULL == jcontract) { + json_decref (root); return TMH_RESPONSE_reply_external_error (connection, "contract request malformed"); } @@ -105,10 +168,49 @@ MH_handler_contract (struct TMH_RequestHandler *rh, jcontract, spec); if (GNUNET_NO == res) + { + json_decref (root); return MHD_YES; + } if (GNUNET_SYSERR == res) + { + json_decref (root); return TMH_RESPONSE_reply_external_error (connection, "contract request malformed"); + } + /* check contract is well-formed */ + if (GNUNET_OK != check_products (products)) + { + GNUNET_JSON_parse_free (spec); + json_decref (root); + return TMH_RESPONSE_reply_external_error (connection, + "products in contract request malformed"); + } + + /* Check if this transaction ID erroneously corresponds to a + contract that already paid, in which case we should refuse + to sign it again (frontend buggy, it should use a fresh + transaction ID each time)! */ + if (GNUNET_OK == + db->check_payment (db->cls, + transaction_id)) + { + struct MHD_Response *resp; + int ret; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction %llu already paid in the past, refusing to sign!\n", + (unsigned long long) transaction_id); + resp = MHD_create_response_from_buffer (strlen ("Duplicate transaction ID!"), + "Duplicate transaction ID!", + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + return ret; + } + /* add fields to the contract that the backend should provide */ json_object_set (jcontract, "exchanges", @@ -126,9 +228,6 @@ MH_handler_contract (struct TMH_RequestHandler *rh, sizeof (pubkey))); /* create contract signature */ - GNUNET_assert (GNUNET_OK == - TALER_JSON_hash (jcontract, - &contract.h_contract)); contract.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT); contract.purpose.size = htonl (sizeof (contract)); contract.transaction_id = GNUNET_htonll (transaction_id); @@ -136,6 +235,9 @@ MH_handler_contract (struct TMH_RequestHandler *rh, &total); TALER_amount_hton (&contract.max_fee, &max_fee); + GNUNET_assert (GNUNET_OK == + TALER_JSON_hash (jcontract, + &contract.h_contract)); GNUNET_CRYPTO_eddsa_sign (privkey, &contract.purpose, &contract_sig); @@ -146,9 +248,10 @@ MH_handler_contract (struct TMH_RequestHandler *rh, "{s:O, s:O, s:O}", "contract", jcontract, "merchant_sig", GNUNET_JSON_from_data (&contract_sig, - sizeof (contract_sig)), + sizeof (contract_sig)), "H_contract", GNUNET_JSON_from_data (&contract.h_contract, - sizeof (contract.h_contract))); + sizeof (contract.h_contract))); + GNUNET_JSON_parse_free (spec); json_decref (root); return res; } diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -312,7 +312,7 @@ return_result (void *cls) */ struct TMH_EXCHANGES_FindOperation * TMH_EXCHANGES_find_exchange (const char *chosen_exchange, - TMH_EXCHANGES_FindContinuation fc, + TMH_EXCHANGES_FindContinuation fc, // process payment void *fc_cls) { struct Exchange *exchange; @@ -390,7 +390,7 @@ TMH_EXCHANGES_find_exchange (const char *chosen_exchange, exchange->fo_tail, fo); - if (GNUNET_YES != exchange->pending) + if (GNUNET_YES != exchange->pending) // can post coins { /* We are not currently waiting for a reply, immediately return result */ diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c @@ -232,6 +232,24 @@ resume_pay_with_response (struct PayContext *pc, 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. @@ -300,10 +318,19 @@ deposit_cb (void *cls, } else { - /* Forward error including 'proof' for the body */ + /* 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 (eproof, + "coin_pub", + GNUNET_JSON_from_data (&dc->coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP))); resume_pay_with_response (pc, http_status, - TMH_RESPONSE_make_json (proof)); + TMH_RESPONSE_make_json (eproof)); + json_decref (eproof); } return; } @@ -436,12 +463,16 @@ process_pay_with_exchange (void *cls, &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:o}", "hint", "unknown denom to exchange", "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, "unknown denom to exchange: %s\n", denom_enc); + GNUNET_free (denom_enc); return; } if (GNUNET_OK != @@ -449,12 +480,16 @@ process_pay_with_exchange (void *cls, 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:o}", "hint", "no acceptable auditor for denomination", "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, "no acceptable auditor for denomination: %s\n", denom_enc); + GNUNET_free (denom_enc); return; } if (0 == i) @@ -633,8 +668,6 @@ MH_handler_pay (struct TMH_RequestHandler *rh, GNUNET_break (0); return MHD_NO; /* hard error */ } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response for /pay.\n"); res = MHD_queue_response (connection, pc->response_code, pc->response); @@ -643,6 +676,10 @@ MH_handler_pay (struct TMH_RequestHandler *rh, 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; } @@ -797,6 +834,9 @@ MH_handler_pay (struct TMH_RequestHandler *rh, /* Payment succeeded in the past; take short cut and accept immediately */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Transaction %llu already paid in the past, taking short cut.\n", + (unsigned long long) pc->transaction_id); resp = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); diff --git a/src/backend/taler-merchant-httpd_pay.h b/src/backend/taler-merchant-httpd_pay.h @@ -23,7 +23,6 @@ #include <microhttpd.h> #include "taler-merchant-httpd.h" - /** * Manage a payment * diff --git a/src/backend/taler-merchant-httpd_util.c b/src/backend/taler-merchant-httpd_util.c @@ -29,7 +29,6 @@ #include "taler-merchant-httpd_responses.h" - /** * Hashes a plain JSON contract sending the result to the other end of * HTTP communication @@ -48,7 +47,6 @@ MH_handler_hash_contract (struct TMH_RequestHandler *rh, const char *upload_data, size_t *upload_data_size) { - json_t *root; json_t *jcontract; int res; @@ -102,8 +100,7 @@ MH_handler_hash_contract (struct TMH_RequestHandler *rh, MHD_HTTP_OK, "{s:O}", "hash", GNUNET_JSON_from_data (&hc, - sizeof (hc))); + sizeof (hc))); json_decref (root); return res; - } diff --git a/src/backend/test-merchant.conf b/src/backend/test-merchant.conf diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -48,6 +48,26 @@ struct PostgresClosure /** + * Drop merchant tables + * + * @param cls closure our `struct Plugin` + * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure + */ +static int +postgres_drop_tables (void *cls) +{ + struct PostgresClosure *pg = cls; + int ret; + + ret = GNUNET_POSTGRES_exec (pg->conn, + "DROP TABLE payments;"); + if (GNUNET_OK != ret) + return ret; + return GNUNET_OK; +} + + +/** * Initialize merchant tables * * @param cls closure our `struct Plugin` @@ -290,6 +310,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) pg->conn = GNUNET_POSTGRES_connect (cfg, "merchantdb-postgres"); plugin = GNUNET_new (struct TALER_MERCHANTDB_Plugin); plugin->cls = pg; + plugin->drop_tables = &postgres_drop_tables; plugin->initialize = &postgres_initialize; plugin->store_payment = &postgres_store_payment; plugin->check_payment = &postgres_check_payment; diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -25,6 +25,65 @@ #include <gnunet/gnunet_curl_lib.h> #include <jansson.h> +/* ********************* /contract *********************** */ + + +/** + * @brief Handle to a /contract operation at a merchant's backend. + */ +struct TALER_MERCHANT_ContractOperation; + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls closure + * @param http_status HTTP response code, 200 indicates success; + * 0 if the backend's reply is bogus (fails to follow the protocol) + * @param obj the full received JSON reply, or + * error details if the request failed + * @param contract completed contract, NULL on error + * @param sig merchant's signature over the contract, NULL on error + * @param h_contract hash of the contract, NULL on error + */ +typedef void +(*TALER_MERCHANT_ContractCallback) (void *cls, + unsigned int http_status, + const json_t *obj, + const json_t *contract, + const struct TALER_MerchantSignatureP *sig, + const struct GNUNET_HashCode *h_contract); + + +/** + * Request backend to sign a contract (and add fields like wire transfer + * details). + * + * @param ctx execution context + * @param backend_uri URI of the backend + * @param contract prototype of the contract + * @param contract_cb the callback to call when a reply for this request is available + * @param contract_cb_cls closure for @a contract_cb + * @return a handle for this request + */ +struct TALER_MERCHANT_ContractOperation * +TALER_MERCHANT_contract_sign (struct GNUNET_CURL_Context *ctx, + const char *backend_uri, + const json_t *contract, + TALER_MERCHANT_ContractCallback contract_cb, + void *contract_cb_cls); + + +/** + * Cancel a /contract request. + * + * @param co the contract operation handle + */ +void +TALER_MERCHANT_contract_sign_cancel (struct TALER_MERCHANT_ContractOperation *co); + + /* ********************* /pay *********************** */ @@ -49,14 +108,11 @@ struct TALER_MERCHANT_Pay; * can indicate success, depending on whether the interaction * was with a merchant frontend or backend; * 0 if the merchant's reply is bogus (fails to follow the protocol) - * @param redirect_uri URI for the redirect, if the request was successful and we were talking to a frontend; - * NULL if the request failed or if were were talking to a backend * @param obj the received JSON reply, with error details if the request failed */ typedef void (*TALER_MERCHANT_PayCallback) (void *cls, unsigned int http_status, - const char *redirect_uri, const json_t *obj); @@ -77,6 +133,11 @@ struct TALER_MERCHANT_PayCoin struct TALER_DenominationSignature denom_sig; /** + * Overall value that coins of this @e denom_pub have. + */ + struct TALER_Amount denom_value; + + /** * Coin's private key. */ struct TALER_CoinSpendPrivateKeyP coin_priv; @@ -120,7 +181,7 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx, const char *merchant_uri, const struct GNUNET_HashCode *h_contract, uint64_t transaction_id, - const struct TALER_Amount *amount, + const struct TALER_Amount *amount, const struct TALER_Amount *max_fee, const struct TALER_MerchantPublicKeyP *merchant_pub, const struct TALER_MerchantSignatureP *merchant_sig, @@ -151,6 +212,11 @@ struct TALER_MERCHANT_PaidCoin struct TALER_DenominationSignature denom_sig; /** + * Overall value that coins of this @e denom_pub have. + */ + struct TALER_Amount denom_value; + + /** * Coin's public key. */ struct TALER_CoinSpendPublicKeyP coin_pub; @@ -185,12 +251,10 @@ struct TALER_MERCHANT_PaidCoin * @param amount total value of the contract to be paid to the merchant * @param max_fee maximum fee covered by the merchant (according to the contract) * @param transaction_id transaction id for the transaction between merchant and customer - * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) * @param merchant_sig the signature of the merchant over the original contract * @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed) * @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant - * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend) - * @param h_wire hash of the merchant’s account details + * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend). If non-zero, must be larger than @a refund_deadline. * @param exchange_uri URI of the exchange that the coins belong to * @param num_coins number of coins used to pay * @param coins array of coins we use to pay @@ -206,12 +270,10 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, const struct TALER_Amount *amount, const struct TALER_Amount *max_fee, uint64_t transaction_id, - const struct TALER_MerchantPublicKeyP *merchant_pub, const struct TALER_MerchantSignatureP *merchant_sig, struct GNUNET_TIME_Absolute refund_deadline, struct GNUNET_TIME_Absolute timestamp, struct GNUNET_TIME_Absolute execution_deadline, - const struct GNUNET_HashCode *h_wire, const char *exchange_uri, unsigned int num_coins, const struct TALER_MERCHANT_PaidCoin *coins, diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -48,6 +48,15 @@ struct TALER_MERCHANTDB_Plugin char *library_name; /** + * Drop merchant tables. Used for testcases. + * + * @param cls closure + * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure + */ + int + (*drop_tables) (void *cls); + + /** * Initialize merchant tables * * @param cls closure diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -14,9 +14,11 @@ libtalermerchant_la_LDFLAGS = \ -no-undefined libtalermerchant_la_SOURCES = \ + merchant_api_contract.c \ merchant_api_pay.c libtalermerchant_la_LIBADD = \ + -ltalerexchange \ -ltalerjson \ -ltalerutil \ -lgnunetcurl \ @@ -42,11 +44,13 @@ TESTS = \ test_merchant_api_SOURCES = \ test_merchant_api.c test_merchant_api_LDADD = \ + $(top_srcdir)/src/backenddb/libtalermerchantdb.la \ libtalermerchant.la \ $(LIBGCRYPT_LIBS) \ -ltalerexchange \ -ltalerjson \ -ltalerutil \ + -lgnunetjson \ -lgnunetcurl \ -lgnunetutil \ -ljansson diff --git a/src/lib/merchant_api_contract.c b/src/lib/merchant_api_contract.c @@ -0,0 +1,238 @@ +/* + This file is part of TALER + Copyright (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 Lesser General Public License as published by the Free Software + Foundation; either version 2.1, 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/merchant_api_contract.c + * @brief Implementation of the /contract request of the merchant's HTTP API + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> + + +/** + * @brief A Contract Operation Handle + */ +struct TALER_MERCHANT_ContractOperation +{ + + /** + * The url for this request. + */ + char *url; + + /** + * JSON encoding of the request to POST. + */ + char *json_enc; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_ContractCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP /contract request. + * + * @param cls the `struct TALER_MERCHANT_Pay` + * @param response_code HTTP response code, 0 on error + * @param json response body, NULL if not in JSON + */ +static void +handle_contract_finished (void *cls, + long response_code, + const json_t *json) +{ + struct TALER_MERCHANT_ContractOperation *co = cls; + json_t *contract; + const struct TALER_MerchantSignatureP *sigp; + const struct GNUNET_HashCode *h_contractp; + struct TALER_MerchantSignatureP sig; + struct GNUNET_HashCode h_contract; + + co->job = NULL; + contract = NULL; + sigp = NULL; + h_contractp = NULL; + switch (response_code) + { + case 0: + break; + case MHD_HTTP_OK: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("contract", &contract), + GNUNET_JSON_spec_fixed_auto ("merchant_sig", &sig), + GNUNET_JSON_spec_fixed_auto ("H_contract", &h_contract), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + response_code = 0; + break; + } + h_contractp = &h_contract; + sigp = &sig; + } + break; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the merchant is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + /* Duplicate transaction ID, frontend is buggy! */ + break; + case MHD_HTTP_UNAUTHORIZED: + /* Nothing really to verify, merchant says one of the signatures is + invalid; as we checked them, this should never happen, we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + (unsigned int) response_code); + GNUNET_break (0); + response_code = 0; + break; + } + co->cb (co->cb_cls, + response_code, + json, + contract, + sigp, + h_contractp); + if (NULL != contract) + json_decref (contract); + TALER_MERCHANT_contract_sign_cancel (co); +} + + +/** + * Request backend to sign a contract (and add fields like wire transfer + * details). + * + * @param ctx execution context + * @param backend_uri URI of the backend + * @param contract prototype of the contract + * @param contract_cb the callback to call when a reply for this request is available + * @param contract_cb_cls closure for @a contract_cb + * @return a handle for this request + */ +struct TALER_MERCHANT_ContractOperation * +TALER_MERCHANT_contract_sign (struct GNUNET_CURL_Context *ctx, + const char *backend_uri, + const json_t *contract, + TALER_MERCHANT_ContractCallback contract_cb, + void *contract_cb_cls) +{ + struct TALER_MERCHANT_ContractOperation *co; + json_t *req; + CURL *eh; + + co = GNUNET_new (struct TALER_MERCHANT_ContractOperation); + co->ctx = ctx; + co->cb = contract_cb; + co->cb_cls = contract_cb_cls; + co->url = GNUNET_strdup (backend_uri); + + req = json_pack ("{s:O}", + "contract", (json_t *) contract); + eh = curl_easy_init (); + GNUNET_assert (NULL != (co->json_enc = + json_dumps (req, + JSON_COMPACT))); + json_decref (req); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + co->url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + co->json_enc)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDSIZE, + strlen (co->json_enc))); + co->job = GNUNET_CURL_job_add (ctx, + eh, + GNUNET_YES, + &handle_contract_finished, + co); + return co; +} + + +/** + * Cancel a /contract request. This function cannot be used + * on a request handle if a response is already served for it. + * + * @param co the contract operation request handle + */ +void +TALER_MERCHANT_contract_sign_cancel (struct TALER_MERCHANT_ContractOperation *co) +{ + if (NULL != co->job) + { + GNUNET_CURL_job_cancel (co->job); + co->job = NULL; + } + GNUNET_free (co->url); + GNUNET_free (co->json_enc); + GNUNET_free (co); +} + + +/* end of merchant_api_contract.c */ diff --git a/src/lib/merchant_api_pay.c b/src/lib/merchant_api_pay.c @@ -28,6 +28,7 @@ #include "taler_merchant_service.h" #include <taler/taler_json_lib.h> #include <taler/taler_signatures.h> +#include <taler/taler_exchange_service.h> /** @@ -65,9 +66,113 @@ struct TALER_MERCHANT_Pay * Reference to the execution context. */ struct GNUNET_CURL_Context *ctx; + + /** + * Number of @e coins we are paying with. + */ + unsigned int num_coins; + + /** + * The coins we are paying with. + */ + struct TALER_MERCHANT_PaidCoin *coins; + }; +/** + * We got a 403 response back from the exchange (or the merchant). + * Now we need to check the provided cryptographic proof that the + * coin was actually already spent! + * + * @param pc handle of the original coin we paid with + * @param json cryptographic proof of coin's transaction history as + * was returned by the exchange/merchant + * @return #GNUNET_OK if proof checks out + */ +static int +check_coin_history (const struct TALER_MERCHANT_PaidCoin *pc, + json_t *json) +{ + struct TALER_Amount spent; + struct TALER_Amount spent_plus_contrib; + + if (GNUNET_OK != + TALER_EXCHANGE_verify_coin_history (pc->amount_with_fee.currency, + &pc->coin_pub, + json, + &spent)) + { + /* Exchange's history fails to verify */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&spent_plus_contrib, + &spent, + &pc->amount_with_fee)) + { + /* We got an integer overflow? Bad application! */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (-1 != TALER_amount_cmp (&pc->denom_value, + &spent_plus_contrib)) + { + /* according to our calculations, the transaction should + have still worked, exchange error! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Accepting proof of double-spending\n"); + return GNUNET_OK; +} + + +/** + * We got a 403 response back from the exchange (or the merchant). + * Now we need to check the provided cryptographic proof that the + * coin was actually already spent! + * + * @param ph handle of the original pay operation + * @param json cryptographic proof returned by the exchange/merchant + * @return #GNUNET_OK if proof checks out + */ +static int +check_forbidden (struct TALER_MERCHANT_Pay *ph, + const json_t *json) +{ + json_t *history; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("history", &history), + GNUNET_JSON_spec_fixed_auto ("coin_pub", &coin_pub), + GNUNET_JSON_spec_end() + }; + unsigned int i; + + if (GNUNET_OK != + GNUNET_JSON_parse (json, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + for (i=0;i<ph->num_coins;i++) + { + if (0 == memcmp (&ph->coins[i].coin_pub, + &coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP))) + return check_coin_history (&ph->coins[i], + history); + } + GNUNET_break_op (0); /* complaint is not about any of the coins + that we actually paid with... */ + return GNUNET_SYSERR; +} + /** * Function called when we're done processing the @@ -96,6 +201,12 @@ handle_pay_finished (void *cls, (or API version conflict); just pass JSON reply to the application */ break; case MHD_HTTP_FORBIDDEN: + if (GNUNET_OK != check_forbidden (ph, + json)) + { + GNUNET_break_op (0); + response_code = 0; + } break; case MHD_HTTP_UNAUTHORIZED: /* Nothing really to verify, merchant says one of the signatures is @@ -119,9 +230,11 @@ handle_pay_finished (void *cls, response_code = 0; break; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "/pay completed with response code %u\n", + (unsigned int) response_code); ph->cb (ph->cb_cls, response_code, - "FIXME-redirect-URI", json); TALER_MERCHANT_pay_cancel (ph); } @@ -134,6 +247,11 @@ handle_pay_finished (void *cls, * @param exchange_uri URI of the exchange that the coins belong to * @param h_wire hash of the merchant’s account details * @param h_contract hash of the contact of the merchant with the customer + * @param transaction_id transaction id for the transaction between merchant and customer + * @param amount total value of the contract to be paid to the merchant + * @param max_fee maximum fee covered by the merchant (according to the contract) + * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) + * @param merchant_sig signature from the merchant over the original contract * @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant * @param transaction_id transaction id for the transaction between merchant and customer * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) @@ -205,6 +323,7 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx, &p->coin_sig.eddsa_signature); p->denom_pub = coin->denom_pub; p->denom_sig = coin->denom_sig; + p->denom_value = coin->denom_value; p->coin_pub = dr.coin_pub; p->amount_with_fee = coin->amount_with_fee; p->amount_without_fee = coin->amount_without_fee; @@ -215,12 +334,10 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx, amount, max_fee, transaction_id, - merchant_pub, merchant_sig, refund_deadline, timestamp, GNUNET_TIME_UNIT_ZERO_ABS, - h_wire, exchange_uri, num_coins, pc, @@ -237,13 +354,11 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx, * * @param ctx the execution loop context * @param exchange_uri URI of the exchange that the coins belong to - * @param h_wire hash of the merchant’s account details * @param h_contract hash of the contact of the merchant with the customer * @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant * @param transaction_id transaction id for the transaction between merchant and customer - * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) * @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed) - * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend) + * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend). If non-zero, must be larger than @a refund_deadline. * @param num_coins number of coins used to pay * @param coins array of coins we use to pay * @param coin_sig the signature made with purpose #TALER_SIGNATURE_WALLET_COIN_DEPOSIT made by the customer with the coin’s private key. @@ -260,12 +375,10 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, const struct TALER_Amount *amount, const struct TALER_Amount *max_fee, uint64_t transaction_id, - const struct TALER_MerchantPublicKeyP *merchant_pub, const struct TALER_MerchantSignatureP *merchant_sig, struct GNUNET_TIME_Absolute refund_deadline, struct GNUNET_TIME_Absolute timestamp, struct GNUNET_TIME_Absolute execution_deadline, - const struct GNUNET_HashCode *h_wire, const char *exchange_uri, unsigned int num_coins, const struct TALER_MERCHANT_PaidCoin *coins, @@ -280,6 +393,19 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, struct TALER_Amount total_amount; unsigned int i; + if (GNUNET_YES != + TALER_amount_cmp_currency (amount, + max_fee)) + { + GNUNET_break (0); + return NULL; + } + if ( (0 != execution_deadline.abs_value_us) && + (execution_deadline.abs_value_us < refund_deadline.abs_value_us) ) + { + GNUNET_break (0); + return NULL; + } if (0 == num_coins) { GNUNET_break (0); @@ -364,6 +490,14 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, json_decref (j_coins); return NULL; } + if (GNUNET_YES != + TALER_amount_cmp_currency (&new_amount, + &total_amount)) + { + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } if (1 == TALER_amount_cmp (&new_amount, &total_amount)) @@ -380,6 +514,14 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, { /* Full fee covered by merchant, but our total must at least cover the total contract amount */ + if (GNUNET_YES != + TALER_amount_cmp_currency (amount, + &total_amount)) + { + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } if (1 == TALER_amount_cmp (amount, &total_amount)) @@ -424,6 +566,12 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx, ph->cb = pay_cb; ph->cb_cls = pay_cb_cls; ph->url = GNUNET_strdup (merchant_uri); + ph->num_coins = num_coins; + ph->coins = GNUNET_new_array (num_coins, + struct TALER_MERCHANT_PaidCoin); + memcpy (ph->coins, + coins, + num_coins * sizeof (struct TALER_MERCHANT_PaidCoin)); eh = curl_easy_init (); GNUNET_assert (NULL != (ph->json_enc = diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c @@ -24,6 +24,7 @@ #include <taler/taler_exchange_service.h> #include <taler/taler_json_lib.h> #include "taler_merchant_service.h" +#include "taler_merchantdb_lib.h" #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_curl_lib.h> #include <microhttpd.h> @@ -96,6 +97,11 @@ enum OpCode OC_WITHDRAW_SIGN, /** + * Get backend to sign a contract. + */ + OC_CONTRACT, + + /** * Pay with coins. */ OC_PAY @@ -287,21 +293,49 @@ struct Command } reserve_withdraw; /** - * Information for a #OC_PAY command. - * FIXME: support tests where we pay with multiple coins at once. + * Information for an #OC_CONTRACT command. */ struct { /** - * Amount to pay (total for the entire contract). + * Contract proposal (without merchant_pub, exchanges or H_wire). + */ + const char *proposal; + + /** + * Handle to the active /contract operation, or NULL. + */ + struct TALER_MERCHANT_ContractOperation *co; + + /** + * Full contract in JSON, set by the /contract operation. */ - const char *total_amount; + json_t *contract; /** - * Maximum fee covered by merchant. + * Signature, set by the /contract operation. */ - const char *max_fee; + struct TALER_MerchantSignatureP merchant_sig; + + /** + * Hash of the full contract, set by the /contract operation. + */ + struct GNUNET_HashCode h_contract; + + } contract; + + /** + * Information for a #OC_PAY command. + * FIXME: support tests where we pay with multiple coins at once. + */ + struct + { + + /** + * Reference to the contract. + */ + const char *contract_ref; /** * Reference to a reserve_withdraw operation for a coin to @@ -330,27 +364,6 @@ struct Command const char *amount_without_fee; /** - * JSON string describing the merchant's "wire details". - */ - const char *wire_details; - - /** - * JSON string describing the contract between the two parties. - */ - const char *contract; - - /** - * Transaction ID to use. - */ - uint64_t transaction_id; - - /** - * Relative time (to add to 'now') to compute the refund deadline. - * Zero for no refunds. - */ - struct GNUNET_TIME_Relative refund_deadline; - - /** * Deposit handle while operation is running. */ struct TALER_MERCHANT_Pay *ph; @@ -399,6 +412,10 @@ struct InterpreterState static void fail (struct InterpreterState *is) { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Interpreter failed at step %s (#%u)\n", + is->commands[is->ip].label, + is->ip); result = GNUNET_SYSERR; GNUNET_SCHEDULER_shutdown (); } @@ -716,6 +733,54 @@ reserve_withdraw_cb (void *cls, /** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls closure + * @param http_status HTTP response code, 200 indicates success; + * 0 if the backend's reply is bogus (fails to follow the protocol) + * @param obj the full received JSON reply, or + * error details if the request failed + * @param contract completed contract, NULL on error + * @param sig merchant's signature over the contract, NULL on error + * @param h_contract hash of the contract, NULL on error + */ +static void +contract_cb (void *cls, + unsigned int http_status, + const json_t *obj, + const json_t *contract, + const struct TALER_MerchantSignatureP *sig, + const struct GNUNET_HashCode *h_contract) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + + cmd->details.contract.co = NULL; + switch (http_status) + { + case MHD_HTTP_OK: + cmd->details.contract.contract = json_incref ((json_t *) contract); + cmd->details.contract.merchant_sig = *sig; + cmd->details.contract.h_contract = *h_contract; + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "/contract responded with unexpected status code %u in step %u\n", + http_status, + is->ip); + json_dumpf (obj, stderr, 0); + GNUNET_break (0); + fail (is); + return; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** * Function called with the result of a /pay operation. * * @param cls closure with the interpreter state @@ -727,7 +792,6 @@ reserve_withdraw_cb (void *cls, static void pay_cb (void *cls, unsigned int http_status, - const char *redirect_uri, const json_t *obj) { struct InterpreterState *is = cls; @@ -977,83 +1041,93 @@ interpreter_run (void *cls) return; } return; - case OC_PAY: + case OC_CONTRACT: { - struct TALER_MERCHANT_PayCoin pc; - struct TALER_Amount amount; - struct TALER_Amount max_fee; - json_t *wire; - json_t *contract; - struct GNUNET_HashCode h_wire; - struct GNUNET_HashCode h_contract; - struct GNUNET_TIME_Absolute refund_deadline; - struct GNUNET_TIME_Absolute timestamp; - struct TALER_MerchantSignatureP merchant_sig; - - /* get amount */ - if (GNUNET_OK != - TALER_string_to_amount (cmd->details.pay.total_amount, - &amount)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse total amount `%s' at %u\n", - cmd->details.pay.total_amount, - is->ip); - fail (is); - return; - } - - /* get max_fee */ - if (GNUNET_OK != - TALER_string_to_amount (cmd->details.pay.max_fee, - &max_fee)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse max_fee `%s' at %u\n", - cmd->details.pay.max_fee, - is->ip); - fail (is); - return; - } + json_t *proposal; + json_error_t error; /* parse wire details */ - wire = json_loads (cmd->details.pay.wire_details, - JSON_REJECT_DUPLICATES, - NULL); - if (NULL == wire) + proposal = json_loads (cmd->details.contract.proposal, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == proposal) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse wire details `%s' at %u\n", - cmd->details.pay.wire_details, - is->ip); + "Failed to parse proposal `%s' at command #%u: %s at %u\n", + cmd->details.contract.proposal, + is->ip, + error.text, + (unsigned int) error.column); fail (is); return; } - TALER_JSON_hash (wire, - &h_wire); - json_decref (wire); - - /* parse contract */ - contract = json_loads (cmd->details.pay.contract, - JSON_REJECT_DUPLICATES, - NULL); - if (NULL == contract) + cmd->details.contract.co + = TALER_MERCHANT_contract_sign (ctx, + MERCHANT_URI "contract", + proposal, + &contract_cb, + is); + if (NULL == cmd->details.contract.co) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse contract details `%s' at instruction %u\n", - cmd->details.pay.contract, - is->ip); + GNUNET_break (0); fail (is); return; } - TALER_JSON_hash (contract, + return; + } + case OC_PAY: + { + struct TALER_MERCHANT_PayCoin pc; + uint64_t transaction_id; + struct GNUNET_TIME_Absolute refund_deadline; + struct GNUNET_TIME_Absolute timestamp; + struct GNUNET_HashCode h_wire; + struct TALER_MerchantPublicKeyP merchant_pub; + struct TALER_MerchantSignatureP merchant_sig; + struct GNUNET_HashCode h_contract; + struct TALER_Amount total_amount; + struct TALER_Amount max_fee; + const char *error_name; + unsigned int error_line; + + /* get amount */ + ref = find_command (is, + cmd->details.pay.contract_ref); + merchant_sig = ref->details.contract.merchant_sig; + GNUNET_assert (NULL != ref->details.contract.contract); + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_uint64 ("transaction_id", &transaction_id), + GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), + GNUNET_JSON_spec_absolute_time ("timestamp", &timestamp), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub), + GNUNET_JSON_spec_fixed_auto ("H_wire", &h_wire), + TALER_JSON_spec_amount ("amount", &total_amount), + TALER_JSON_spec_amount ("max_fee", &max_fee), + GNUNET_JSON_spec_end() + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (ref->details.contract.contract, + spec, + &error_name, + &error_line)) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u\n", + error_name, + error_line); + fail (is); + return; + } + } + + TALER_JSON_hash (ref->details.contract.contract, &h_contract); - json_decref (contract); /* initialize 'pc' (FIXME: to do in a loop later...) */ { - const struct Command *ref; - memset (&pc, 0, sizeof (pc)); ref = find_command (is, cmd->details.pay.coin_ref); @@ -1064,6 +1138,7 @@ interpreter_run (void *cls) pc.coin_priv = ref->details.reserve_withdraw.coin_priv; pc.denom_pub = ref->details.reserve_withdraw.pk->key; pc.denom_sig = ref->details.reserve_withdraw.sig; + pc.denom_value = ref->details.reserve_withdraw.pk->value; break; default: GNUNET_assert (0); @@ -1094,21 +1169,12 @@ interpreter_run (void *cls) } } - if (0 == cmd->details.pay.refund_deadline.rel_value_us) - refund_deadline = GNUNET_TIME_UNIT_ZERO_ABS; /* no refunds */ - else - refund_deadline = GNUNET_TIME_relative_to_absolute (cmd->details.pay.refund_deadline); - GNUNET_TIME_round_abs (&refund_deadline); - timestamp = GNUNET_TIME_absolute_get (); - GNUNET_TIME_round_abs (&timestamp); - memset (&merchant_sig, 0, sizeof (merchant_sig)); // FIXME: init properly! - GNUNET_break (0); cmd->details.pay.ph = TALER_MERCHANT_pay_wallet (ctx, MERCHANT_URI "pay", &h_contract, - cmd->details.pay.transaction_id, - &amount, + transaction_id, + &total_amount, &max_fee, &merchant_pub, &merchant_sig, @@ -1225,6 +1291,22 @@ do_shutdown (void *cls) cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key = NULL; } break; + case OC_CONTRACT: + if (NULL != cmd->details.contract.co) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command %u (%s) did not complete\n", + i, + cmd->label); + TALER_MERCHANT_contract_sign_cancel (cmd->details.contract.co); + cmd->details.contract.co = NULL; + } + if (NULL != cmd->details.contract.contract) + { + json_decref (cmd->details.contract.contract); + cmd->details.contract.contract = NULL; + } + break; case OC_PAY: if (NULL != cmd->details.pay.ph) { @@ -1335,57 +1417,33 @@ run (void *cls) .expected_response_code = MHD_HTTP_OK, .details.reserve_status.reserve_reference = "create-reserve-1", .details.reserve_status.expected_balance = "EUR:0" }, - /* Try to pay with the 5 EUR coin (in full) */ + /* Create contract */ + { .oc = OC_CONTRACT, + .label = "create-contract-1", + .expected_response_code = MHD_HTTP_OK, + .details.contract.proposal = "{ \"max_fee\":{\"currency\":\"EUR\", \"value\":0, \"fraction\":500000}, \"transaction_id\":1, \"timestamp\":\"\\/Date(42)\\/\", \"refund_deadline\":\"\\/Date(0)\\/\", \"expiry\":\"\\/Date(999999999)\\/\", \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0}, \"products\":[ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }" }, { .oc = OC_PAY, .label = "deposit-simple", .expected_response_code = MHD_HTTP_OK, - .details.pay.total_amount = "EUR:5", - .details.pay.max_fee = "EUR:0.5", + .details.pay.contract_ref = "create-contract-1", .details.pay.coin_ref = "withdraw-coin-1", .details.pay.amount_with_fee = "EUR:5", - .details.pay.amount_without_fee = "EUR:4.99", - .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }", - .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }", - .details.pay.transaction_id = 1 }, + .details.pay.amount_without_fee = "EUR:4.99" }, + /* Create another contract */ + { .oc = OC_CONTRACT, + .label = "create-contract-2", + .expected_response_code = MHD_HTTP_OK, + .details.contract.proposal = "{ \"max_fee\":{\"currency\":\"EUR\", \"value\":0, \"fraction\":500000}, \"transaction_id\":2, \"timestamp\":\"\\/Date(42)\\/\", \"refund_deadline\":\"\\/Date(0)\\/\", \"expiry\":\"\\/Date(999999999)\\/\", \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0}, \"products\":[ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }" }, - /* Try to double-spend the 5 EUR coin with different wire details */ - { .oc = OC_PAY, - .label = "deposit-double-1", - .expected_response_code = MHD_HTTP_FORBIDDEN, - .details.pay.total_amount = "EUR:5", - .details.pay.max_fee = "EUR:0.5", - .details.pay.coin_ref = "withdraw-coin-1", - .details.pay.amount_with_fee = "EUR:5", - .details.pay.amount_without_fee = "EUR:4.99", - .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }", - .details.pay.contract = "{ \"items\":[{ \"name\":\"ice cream\", \"value\":1} ] }", - .details.pay.transaction_id = 1 }, /* Try to double-spend the 5 EUR coin at the same merchant (but different transaction ID) */ { .oc = OC_PAY, .label = "deposit-double-2", .expected_response_code = MHD_HTTP_FORBIDDEN, - .details.pay.total_amount = "EUR:5", - .details.pay.max_fee = "EUR:0.5", + .details.pay.contract_ref = "create-contract-2", .details.pay.coin_ref = "withdraw-coin-1", .details.pay.amount_with_fee = "EUR:5", - .details.pay.amount_without_fee = "EUR:4.99", - .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }", - .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }", - .details.pay.transaction_id = 2 }, - /* Try to double-spend the 5 EUR coin at the same merchant (but different - contract) */ - { .oc = OC_PAY, - .label = "deposit-double-3", - .expected_response_code = MHD_HTTP_FORBIDDEN, - .details.pay.total_amount = "EUR:5", - .details.pay.max_fee = "EUR:0.5", - .details.pay.coin_ref = "withdraw-coin-1", - .details.pay.amount_with_fee = "EUR:5", - .details.pay.amount_without_fee = "EUR:4.99", - .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }", - .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":2} ] }", - .details.pay.transaction_id = 1 }, + .details.pay.amount_without_fee = "EUR:4.99" }, { .oc = OC_END } }; @@ -1428,12 +1486,35 @@ main (int argc, struct GNUNET_OS_Process *proc; struct GNUNET_OS_Process *exchanged; struct GNUNET_OS_Process *merchantd; + struct TALER_MERCHANTDB_Plugin *db; + struct GNUNET_CONFIGURATION_Handle *cfg; unsetenv ("XDG_DATA_HOME"); unsetenv ("XDG_CONFIG_HOME"); GNUNET_log_setup ("test-merchant-api", "WARNING", NULL); + cfg = GNUNET_CONFIGURATION_create (); + GNUNET_assert (GNUNET_OK == + GNUNET_CONFIGURATION_load (cfg, + "test_merchant_api.conf")); + db = TALER_MERCHANTDB_plugin_load (cfg); + if (NULL == db) + { + GNUNET_CONFIGURATION_destroy (cfg); + return 77; + } + (void) db->drop_tables (db->cls); + if (GNUNET_OK != db->initialize (db->cls)) + { + TALER_MERCHANTDB_plugin_unload (db); + GNUNET_CONFIGURATION_destroy (cfg); + return 77; + } + TALER_MERCHANTDB_plugin_unload (db); + GNUNET_CONFIGURATION_destroy (cfg); + + GNUNET_assert (GNUNET_OK == GNUNET_STRINGS_string_to_data (merchant_pub_str, strlen (merchant_pub_str), diff --git a/src/lib/test_merchant_api.conf b/src/lib/test_merchant_api.conf @@ -4,6 +4,10 @@ # Persistant data storage for the testcase TALER_TEST_HOME = test_merchant_api_home/ +########################################## +# Configuration for the merchant backend # +########################################## + [merchant] # Which port do we run the backend on? (HTTP server) @@ -29,7 +33,7 @@ DB = postgres # Which wireformat do we use? WIREFORMAT = test -[exchange-taler] +[merchant-exchange-test] URI = http://localhost:8081/ MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG @@ -37,7 +41,7 @@ MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG # Auditors must be in sections "auditor-", the rest of the section # name could be anything. -[auditor-ezb] +[merchant-auditor-ezb] # Informal name of the auditor. Just for the user. NAME = European Central Bank @@ -49,6 +53,8 @@ URI = http://taler.ezb.eu/ # This is the important bit: the signing key of the auditor. PUBLIC_KEY = 9QXF7XY7E9VPV47B5Z806NDFSX2VJ79SVHHD29QEQ3BG31ANHZ60 + + # This specifies which database we use. [merchantdb-postgres] CONFIG = postgres:///talercheck @@ -62,6 +68,10 @@ SALT = 17919252168512238964 ADDRESS = "Garching" +################################################### +# Configuration for the exchange for the testcase # +################################################### + [exchange] # How to access our database DB = postgres @@ -86,6 +96,7 @@ BANK_URI = "http://localhost/" # Into which account at the 'bank' should (incoming) wire transfers be made? BANK_ACCOUNT_NUMBER = 2 + [coin_eur_ct_1] value = EUR:0.01 duration_overlap = 5 minutes