/* 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, see */ /** * @file merchant/test_merchant_api.c * @brief testcase to test merchant's HTTP API interface * @author Christian Grothoff * @author Marcello Stanisci */ #include "platform.h" #include #include #include #include #include #include "taler_merchant_service.h" #include "taler_merchantdb_lib.h" #include #include #include /** * URI under which the merchant is reachable during the testcase. */ #define MERCHANT_URI "http://localhost:8082" /** * URI under which the exchange is reachable during the testcase. */ #define EXCHANGE_URI "http://localhost:8081/" /** * URI of the bank. */ #define BANK_URI "http://localhost:8083/" /** * On which port do we run the (fake) bank? */ #define BANK_PORT 8083 /** * Max size allowed for an order. */ #define ORDER_MAX_SIZE 1000 #define RND_BLK(ptr) \ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (*ptr)) /** * Handle to database. */ struct TALER_MERCHANTDB_Plugin *db; /** * Configuration handle. */ struct GNUNET_CONFIGURATION_Handle *cfg; /** * Handle to access the exchange. */ static struct TALER_EXCHANGE_Handle *exchange; /** * Main execution context for the main loop of the exchange. */ static struct GNUNET_CURL_Context *ctx; /** * Array of instances to test against */ static char **instances; /** * How many merchant instances this test runs */ unsigned int ninstances = 0; /** * Current instance */ static char *instance; /** * Current instance being tested */ unsigned int instance_idx = 0; /** * Task run on timeout. */ static struct GNUNET_SCHEDULER_Task *timeout_task; /** * Context for running the #ctx's event loop. */ static struct GNUNET_CURL_RescheduleContext *rc; /** * Handle to the fake bank service we run for the * aggregator. */ static struct TALER_FAKEBANK_Handle *fakebank; /** * Result of the testcases, #GNUNET_OK on success */ static int result; /** * Opcodes for the interpreter. */ enum OpCode { /** * Termination code, stops the interpreter loop (with success). */ OC_END = 0, /** * Issue a GET /proposal to the backend. */ OC_PROPOSAL_LOOKUP, /** * Add funds to a reserve by (faking) incoming wire transfer. */ OC_ADMIN_ADD_INCOMING, /** * Check status of a reserve. */ OC_WITHDRAW_STATUS, /** * Withdraw a coin from a reserve. */ OC_WITHDRAW_SIGN, /** * Issue a PUT /proposal to the backend. */ OC_PROPOSAL, /** * Pay with coins. */ OC_PAY, /** * Run the aggregator to execute deposits. */ OC_RUN_AGGREGATOR, /** * Check that the fakebank has received a certain transaction. */ OC_CHECK_BANK_TRANSFER, /** * Check that the fakebank has not received any other transactions. */ OC_CHECK_BANK_TRANSFERS_EMPTY, /** * Retrieve deposit details for a given wire transfer */ OC_TRACK_TRANSFER, /** * Retrieve wire transfer details for a given transaction */ OC_TRACK_TRANSACTION, /** * Test getting transactions based on timestamp */ OC_HISTORY }; /** * Structure specifying details about a coin to be melted. * Used in a NULL-terminated array as part of command * specification. */ struct MeltDetails { /** * Amount to melt (including fee). */ const char *amount; /** * Reference to reserve_withdraw operations for coin to * be used for the /refresh/melt operation. */ const char *coin_ref; }; /** * Information about a fresh coin generated by the refresh operation. */ struct FreshCoin { /** * If @e amount is NULL, this specifies the denomination key to * use. Otherwise, this will be set (by the interpreter) to the * denomination PK matching @e amount. */ const struct TALER_EXCHANGE_DenomPublicKey *pk; /** * Set (by the interpreter) to the exchange's signature over the * coin's public key. */ struct TALER_DenominationSignature sig; /** * Set (by the interpreter) to the coin's private key. */ struct TALER_CoinSpendPrivateKeyP coin_priv; }; /** * Details for a exchange operation to execute. */ struct Command { /** * Opcode of the command. */ enum OpCode oc; /** * Label for the command, can be NULL. */ const char *label; /** * Which response code do we expect for this command? */ unsigned int expected_response_code; /** * Details about the command. */ union { /** * Information for a #OC_ADMIN_ADD_INCOMING command. */ struct { /** * Label to another admin_add_incoming command if we * should deposit into an existing reserve, NULL if * a fresh reserve should be created. */ const char *reserve_reference; /** * String describing the amount to add to the reserve. */ const char *amount; /** * Sender's bank account details (JSON). */ const char *sender_details; /** * Transfer details (JSON) */ const char *transfer_details; /** * Set (by the interpreter) to the reserve's private key * we used to fill the reserve. */ struct TALER_ReservePrivateKeyP reserve_priv; /** * Set to the API's handle during the operation. */ struct TALER_EXCHANGE_AdminAddIncomingHandle *aih; } admin_add_incoming; /** * Information for OC_PROPOSAL_LOOKUP command. */ struct { /** * Reference to the proposal we want to lookup. */ const char *proposal_reference; struct TALER_MERCHANT_ProposalLookupOperation *plo; } proposal_lookup; /** * Information for a #OC_WITHDRAW_STATUS command. */ struct { /** * Label to the #OC_ADMIN_ADD_INCOMING command which * created the reserve. */ const char *reserve_reference; /** * Set to the API's handle during the operation. */ struct TALER_EXCHANGE_ReserveStatusHandle *wsh; /** * Expected reserve balance. */ const char *expected_balance; } reserve_status; /** * Information for a #OC_WITHDRAW_SIGN command. */ struct { /** * Which reserve should we withdraw from? */ const char *reserve_reference; /** * String describing the denomination value we should withdraw. * A corresponding denomination key must exist in the exchange's * offerings. Can be NULL if @e pk is set instead. */ const char *amount; /** * If @e amount is NULL, this specifies the denomination key to * use. Otherwise, this will be set (by the interpreter) to the * denomination PK matching @e amount. */ const struct TALER_EXCHANGE_DenomPublicKey *pk; /** * Set (by the interpreter) to the exchange's signature over the * coin's public key. */ struct TALER_DenominationSignature sig; /** * Set (by the interpreter) to the coin's private key. */ struct TALER_CoinSpendPrivateKeyP coin_priv; /** * Blinding key used for the operation. */ struct TALER_DenominationBlindingKeyP blinding_key; /** * Withdraw handle (while operation is running). */ struct TALER_EXCHANGE_ReserveWithdrawHandle *wsh; } reserve_withdraw; /** * Information for an #OC_PROPOSAL command. */ struct { /** * The order. * It's dynamically generated because we need different transaction_id * for different merchant instances. */ char order[ORDER_MAX_SIZE]; /** * Handle to the active PUT /proposal operation, or NULL. */ struct TALER_MERCHANT_ProposalOperation *po; /** * Full contract in JSON, set by the /contract operation. * FIXME: verify in the code that this bit is actually proposal * data and not the whole proposal. */ json_t *proposal_data; /** * Proposal's signature. */ struct TALER_MerchantSignatureP merchant_sig; /** * Proposal data's hashcode. */ struct GNUNET_HashCode hash; } proposal; /** * 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 * be used for the /deposit operation. */ const char *coin_ref; /** * If this @e coin_ref refers to an operation that generated * an array of coins, this value determines which coin to use. */ unsigned int coin_idx; /** * Amount to pay (from the coin, including fee). */ const char *amount_with_fee; /** * Amount to pay (from the coin, excluding fee). The sum of the * deltas between all @e amount_with_fee and the @e * amount_without_fee must be less than max_fee, and the sum of * the @e amount_with_fee must be larger than the @e * total_amount. */ const char *amount_without_fee; /** * Deposit handle while operation is running. */ struct TALER_MERCHANT_Pay *ph; /** * Hashcode of the proposal data associated to this payment. */ struct GNUNET_HashCode h_proposal_data; /** * Merchant's public key */ struct TALER_MerchantPublicKeyP merchant_pub; } pay; struct { /** * Process for the aggregator. */ struct GNUNET_OS_Process *aggregator_proc; /** * ID of task called whenever we get a SIGCHILD. */ struct GNUNET_SCHEDULER_Task *child_death_task; } run_aggregator; struct { /** * Which amount do we expect to see transferred? */ const char *amount; /** * Which account do we expect to be debited? */ uint64_t account_debit; /** * Which account do we expect to be credited? */ uint64_t account_credit; /** * Set (!) to the wire transfer identifier observed. */ struct TALER_WireTransferIdentifierRawP wtid; } check_bank_transfer; struct { /** * #OC_CHECK_BANK_TRANSFER command from which we should grab * the WTID. */ char *check_bank_ref; /** * #OC_PAY command which we expect in the result. * Since we are tracking a bank transaction, we want to know * which (Taler) deposit is associated with the bank * transaction being tracked now. */ char *expected_pay_ref; /** * Handle to a /track/transfer operation */ struct TALER_MERCHANT_TrackTransferHandle *tdo; } track_transfer; struct { /** * #OC_PAY command from which we should grab * the WTID. */ char *pay_ref; /** * #OC_CHECK_BANK_TRANSFER command which we expect in the result. */ char *expected_transfer_ref; /** * Handle to a /track/transaction operation */ struct TALER_MERCHANT_TrackTransactionHandle *tth; } track_transaction; struct { /** * Date we want retrieved transactions younger than */ struct GNUNET_TIME_Absolute date; /** * How many "rows" we expect in the result */ unsigned int nresult; /** * Handle to the merchant */ /** * Handle to /history request */ struct TALER_MERCHANT_HistoryOperation *ho; } history; } details; }; /** * State of the interpreter loop. */ struct InterpreterState { /** * Keys from the exchange. */ const struct TALER_EXCHANGE_Keys *keys; /** * Commands the interpreter will run. */ struct Command *commands; /** * Interpreter task (if one is scheduled). */ struct GNUNET_SCHEDULER_Task *task; /** * Instruction pointer. Tells #interpreter_run() which * instruction to run next. */ unsigned int ip; }; /** * Pipe used to communicate child death via signal. */ static struct GNUNET_DISK_PipeHandle *sigpipe; /** * The testcase failed, return with an error code. * * @param is interpreter state to clean up */ 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 (); } /** * Find a command by label. * * @param is interpreter state to search * @param label label to look for * @return NULL if command was not found */ static const struct Command * find_command (const struct InterpreterState *is, const char *label) { unsigned int i; const struct Command *cmd; if (NULL == label) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Attempt to lookup command for empty label\n"); return NULL; } for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) if ( (NULL != cmd->label) && (0 == strcmp (cmd->label, label)) ) return cmd; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command not found: %s\n", label); return NULL; } /** * Run the main interpreter loop that performs exchange operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls); /** * Run the next command with the interpreter. * * @param is current interpeter state. */ static void next_command (struct InterpreterState *is) { is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); } /** * Function called upon completion of our /admin/add/incoming request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 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 full_response full response from the exchange (for logging, in case of errors) */ static void add_incoming_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.admin_add_incoming.aih = NULL; if (MHD_HTTP_OK != http_status) { GNUNET_break (0); fail (is); return; } next_command (is); } /** * Callback for a /history request. It's up to this function how * to render the array containing transactions details (FIXME link to * documentation) * * @param cls closure * @param http_status HTTP status returned by the merchant backend * @param ec taler-specific error code * @param json actual body containing history */ static void history_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *json) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; unsigned int nresult; if (MHD_HTTP_OK != http_status) { fail (is); return; } nresult = json_array_size (json); if (nresult != cmd->details.history.nresult) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected number of history entries. Got %d, expected %d\n", nresult, cmd->details.history.nresult); fail (is); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "/history data: %s\n", json_dumps (json, JSON_INDENT (1))); next_command (is); } /** * Check if the given historic event @a h corresponds to the given * command @a cmd. * * @param h event in history * @param cmd an #OC_ADMIN_ADD_INCOMING command * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not */ static int compare_admin_add_incoming_history (const struct TALER_EXCHANGE_ReserveHistory *h, const struct Command *cmd) { struct TALER_Amount amount; if (TALER_EXCHANGE_RTT_DEPOSIT != h->type) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.admin_add_incoming.amount, &amount)); if (0 != TALER_amount_cmp (&amount, &h->amount)) { GNUNET_break_op (0); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Check if the given historic event @a h corresponds to the given * command @a cmd. * * @param h event in history * @param cmd an #OC_WITHDRAW_SIGN command * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not */ static int compare_reserve_withdraw_history (const struct TALER_EXCHANGE_ReserveHistory *h, const struct Command *cmd) { struct TALER_Amount amount; struct TALER_Amount amount_with_fee; if (TALER_EXCHANGE_RTT_WITHDRAWAL != h->type) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.reserve_withdraw.amount, &amount)); GNUNET_assert (GNUNET_OK == TALER_amount_add (&amount_with_fee, &amount, &cmd->details.reserve_withdraw.pk->fee_withdraw)); if (0 != TALER_amount_cmp (&amount_with_fee, &h->amount)) { GNUNET_break_op (0); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Function called with the result of a /reserve/status request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param[in] json original response in JSON format (useful only for diagnostics) * @param balance current balance in the reserve, NULL on error * @param history_length number of entries in the transaction history, 0 on error * @param history detailed transaction history, NULL on error */ static void reserve_status_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *json, const struct TALER_Amount *balance, unsigned int history_length, const struct TALER_EXCHANGE_ReserveHistory *history) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; struct Command *rel; unsigned int i; unsigned int j; struct TALER_Amount amount; cmd->details.reserve_status.wsh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: j = 0; for (i=0;iip;i++) { switch ((rel = &is->commands[i])->oc) { case OC_ADMIN_ADD_INCOMING: if ( ( (NULL != rel->label) && (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->label) ) ) || ( (NULL != rel->details.admin_add_incoming.reserve_reference) && (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->details.admin_add_incoming.reserve_reference) ) ) ) { if (GNUNET_OK != compare_admin_add_incoming_history (&history[j], rel)) { GNUNET_break (0); fail (is); return; } j++; } break; case OC_WITHDRAW_SIGN: if (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->details.reserve_withdraw.reserve_reference)) { if (GNUNET_OK != compare_reserve_withdraw_history (&history[j], rel)) { GNUNET_break (0); fail (is); return; } j++; } break; default: /* unreleated, just skip */ break; } } if (j != history_length) { GNUNET_break (0); fail (is); return; } if (NULL != cmd->details.reserve_status.expected_balance) { GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.reserve_status.expected_balance, &amount)); if (0 != TALER_amount_cmp (&amount, balance)) { GNUNET_break (0); fail (is); return; } } break; default: /* Unsupported status code (by test harness) */ GNUNET_break (0); break; } next_command (is); } /** * Function called upon completion of our /reserve/withdraw request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code * @param sig signature over the coin, NULL on error * @param full_response full response from the exchange (for logging, in case of errors) */ static void reserve_withdraw_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_DenominationSignature *sig, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.reserve_withdraw.wsh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); GNUNET_break (0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: if (NULL == sig) { GNUNET_break (0); fail (is); return; } cmd->details.reserve_withdraw.sig.rsa_signature = GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature); break; case MHD_HTTP_PAYMENT_REQUIRED: /* nothing to check */ break; default: /* Unsupported status code (by test harness) */ GNUNET_break (0); break; } next_command (is); } /** * Callback that works PUT /proposal's output. * * @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 ec taler-specific error code * @param obj the full received JSON reply, or * error details if the request failed * @param proposal_data the order + additional information provided by the * backend, 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 proposal_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *obj, const json_t *proposal_data, const struct TALER_MerchantSignatureP *sig, const struct GNUNET_HashCode *hash) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.proposal.po = NULL; switch (http_status) { case MHD_HTTP_OK: cmd->details.proposal.proposal_data = json_incref ((json_t *) proposal_data); cmd->details.proposal.merchant_sig = *sig; cmd->details.proposal.hash = *hash; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Hashed proposal, '%s'\n", GNUNET_h2s (hash)); break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "unexpected status code from /proposal: %u. Step %u\n", http_status, is->ip); json_dumpf (obj, stderr, 0); GNUNET_break (0); fail (is); return; } next_command (is); } /** * Function called with the result of a /pay operation. * * @param cls closure with the interpreter state * @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 * @param obj the received JSON reply, should be kept as proof (and, in case of errors, * be forwarded to the customer) */ static void pay_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *obj) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; struct PaymentResponsePS mr; struct GNUNET_CRYPTO_EddsaSignature sig; struct GNUNET_HashCode h_proposal_data; const char *error_name; unsigned int error_line; cmd->details.pay.ph = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } if (MHD_HTTP_OK == http_status) { /* Check signature */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("sig", &sig), GNUNET_JSON_spec_fixed_auto ("h_proposal_data", &h_proposal_data), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (obj, 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; } mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK); mr.purpose.size = htonl (sizeof (mr)); mr.h_proposal_data = h_proposal_data; if (GNUNET_OK != GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK, &mr.purpose, &sig, &cmd->details.pay.merchant_pub.eddsa_pub)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Merchant signature given in response to /pay invalid\n"); fail (is); return; } } next_command (is); } /** * Task triggered whenever we receive a SIGCHLD (child * process died). * * @param cls closure, NULL if we need to self-restart */ static void maint_child_death (void *cls) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct GNUNET_DISK_FileHandle *pr; char c[16]; cmd->details.run_aggregator.child_death_task = NULL; pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c))); GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc); GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc); cmd->details.run_aggregator.aggregator_proc = NULL; next_command (is); } /** * Callback for a /track/transfer operation * * @param cls closure for this function * @param http_status HTTP response code returned by the server * @param ec taler-specific error code * @param sign_key exchange key used to sign @a json, or NULL * @param json original json reply (may include signatures, those have then been * validated already) * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error * @param total_amount total amount of the wire transfer, or NULL if the exchange could * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) * @param details_length length of the @a details array * @param details array with details about the combined transactions */ static void track_transfer_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *sign_key, const json_t *json, const struct GNUNET_HashCode *h_wire, const struct TALER_Amount *total_amount, unsigned int details_length, const struct TALER_TrackTransferDetails *details) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.track_transfer.tdo = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: { const struct Command *ref; unsigned int i; int found; /** * Retrieve the deposit operation that is supposed * to have been paid by the wtid used in this operation. * After that, check if that operation is actually mentioned * in the returned data. */ ref = find_command (is, cmd->details.track_transfer.expected_pay_ref); GNUNET_assert (NULL != ref); found = GNUNET_NO; /** * Iterating over the details makes little sense now, * as each payment involves exatcly one coin. */ for (i=0;idetails.pay.amount_without_fee, &amount_without_fee)); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (ref->details.pay.amount_with_fee, &amount_with_fee)); GNUNET_assert (GNUNET_OK == TALER_amount_subtract (&deposit_fee, &amount_with_fee, &amount_without_fee)); /* Find coin ('s public key) associated with the retrieved deposit. Yes, one deposit - one coin. */ cref = find_command (is, ref->details.pay.coin_ref); proposal_ref = find_command (is, ref->details.pay.contract_ref); GNUNET_assert (NULL != cref); GNUNET_assert (NULL != proposal_ref); switch (cref->oc) { case OC_WITHDRAW_SIGN: GNUNET_CRYPTO_eddsa_key_get_public (&cref->details.reserve_withdraw.coin_priv.eddsa_priv, &coin_pub.eddsa_pub); break; default: GNUNET_assert (0); } if ( (0 == memcmp (&details[i].h_proposal_data, &proposal_ref->details.proposal.hash, sizeof (struct GNUNET_HashCode))) && (0 == TALER_amount_cmp (&details[i].coin_value, &amount_with_fee)) && (0 == TALER_amount_cmp (&details[i].coin_fee, &deposit_fee)) && (0 == memcmp (&details[i].coin_pub, &coin_pub, sizeof (struct TALER_CoinSpendPublicKeyP))) ) found = GNUNET_YES; } if (GNUNET_NO == found) { GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } break; } default: break; } next_command (is); } /** * Callback for GET /proposal issued at backend. Just check * whether response code is as expected. * * @param cls closure * @param http_status HTTP status code we got */ static void proposal_lookup_cb (void *cls, unsigned int http_status, const json_t *json) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.proposal_lookup.plo = NULL; if (cmd->expected_response_code != http_status) fail (is); next_command (is); } /** * Function called with detailed wire transfer data. * * @param cls closure * @param http_status HTTP status code we got, 0 on exchange protocol violation * @param ec taler-specific error code * @param json original json reply * @param num_transfers number of wire transfers involved in setting the transaction * @param transfers detailed list of transfers involved and their coins */ static void track_transaction_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *json, unsigned int num_transfers, const struct TALER_MERCHANT_TransactionWireTransfer *transfers) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.track_transaction.tth = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } /* Test result vs. expecations... */ switch (http_status) { case MHD_HTTP_OK: { const struct Command *ref; struct TALER_Amount ea; struct TALER_Amount coin_contribution; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Successful /track/tracking\n"); if (1 != num_transfers) { GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } ref = find_command (is, cmd->details.track_transaction.expected_transfer_ref); GNUNET_assert (NULL != ref); if (0 != memcmp (&ref->details.check_bank_transfer.wtid, &transfers[0].wtid, sizeof (struct TALER_WireTransferIdentifierRawP))) { GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } /* NOTE: this assumes that the wire transfer corresponds to a single coin involved in a pay/deposit. Thus, this invariant may not always hold in the future depending on how the testcases evolve. */ if (1 != transfers[0].num_coins) { GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (ref->details.check_bank_transfer.amount, &ea)); GNUNET_assert (GNUNET_OK == TALER_amount_subtract (&coin_contribution, &transfers[0].coins[0].amount_with_fee, &transfers[0].coins[0].deposit_fee)); if (0 != TALER_amount_cmp (&ea, &coin_contribution)) { GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } break; } default: break; } next_command (is); } /** * Find denomination key matching the given amount. * * @param keys array of keys to search * @param amount coin value to look for * @return NULL if no matching key was found */ static const struct TALER_EXCHANGE_DenomPublicKey * find_pk (const struct TALER_EXCHANGE_Keys *keys, const struct TALER_Amount *amount) { unsigned int i; struct GNUNET_TIME_Absolute now; struct TALER_EXCHANGE_DenomPublicKey *pk; char *str; now = GNUNET_TIME_absolute_get (); for (i=0;inum_denom_keys;i++) { pk = &keys->denom_keys[i]; if ( (0 == TALER_amount_cmp (amount, &pk->value)) && (now.abs_value_us >= pk->valid_from.abs_value_us) && (now.abs_value_us < pk->withdraw_valid_until.abs_value_us) ) return pk; } /* do 2nd pass to check if expiration times are to blame for failure */ str = TALER_amount_to_string (amount); for (i=0;inum_denom_keys;i++) { pk = &keys->denom_keys[i]; if ( (0 == TALER_amount_cmp (amount, &pk->value)) && ( (now.abs_value_us < pk->valid_from.abs_value_us) || (now.abs_value_us > pk->withdraw_valid_until.abs_value_us) ) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Have denomination key for `%s', but with wrong expiration range %llu vs [%llu,%llu)\n", str, (unsigned long long) now.abs_value_us, (unsigned long long) pk->valid_from.abs_value_us, (unsigned long long) pk->withdraw_valid_until.abs_value_us); GNUNET_free (str); return NULL; } } GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No denomination key for amount %s found\n", str); GNUNET_free (str); return NULL; } /** * Run the main interpreter loop that performs exchange operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls) { const struct GNUNET_SCHEDULER_TaskContext *tc; struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *ref; struct TALER_ReservePublicKeyP reserve_pub; struct TALER_CoinSpendPublicKeyP coin_pub; struct TALER_Amount amount; struct GNUNET_TIME_Absolute execution_date; json_t *sender_details; json_t *transfer_details; is->task = NULL; tc = GNUNET_SCHEDULER_get_task_context (); if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) { fprintf (stderr, "Test aborted by shutdown request\n"); fail (is); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Interpreter runs command %u/%s(%u)\n", is->ip, cmd->label, cmd->oc); switch (cmd->oc) { case OC_END: result = GNUNET_OK; if (instance_idx + 1 == ninstances) { GNUNET_SCHEDULER_shutdown (); return; } is->ip = 0; instance_idx++; instance = instances[instance_idx]; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Switching instance: '%s'\n", instance); is->task = GNUNET_SCHEDULER_add_now (interpreter_run, is); return; case OC_PROPOSAL_LOOKUP: { const char *order_id; GNUNET_assert (NULL != cmd->details.proposal_lookup.proposal_reference); ref = find_command (is, cmd->details.proposal_lookup.proposal_reference); GNUNET_assert (NULL != ref); order_id = json_string_value (json_object_get (ref->details.proposal.proposal_data, "order_id")); GNUNET_assert (NULL != (cmd->details.proposal_lookup.plo = TALER_MERCHANT_proposal_lookup (ctx, MERCHANT_URI, order_id, instance, proposal_lookup_cb, is))); } return; case OC_ADMIN_ADD_INCOMING: if (NULL != cmd->details.admin_add_incoming.reserve_reference) { ref = find_command (is, cmd->details.admin_add_incoming.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); cmd->details.admin_add_incoming.reserve_priv = ref->details.admin_add_incoming.reserve_priv; } else { struct GNUNET_CRYPTO_EddsaPrivateKey *priv; priv = GNUNET_CRYPTO_eddsa_key_create (); cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *priv; GNUNET_free (priv); } GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv, &reserve_pub.eddsa_pub); if (GNUNET_OK != TALER_string_to_amount (cmd->details.admin_add_incoming.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.admin_add_incoming.amount, is->ip); fail (is); return; } execution_date = GNUNET_TIME_absolute_get (); GNUNET_TIME_round_abs (&execution_date); sender_details = json_loads (cmd->details.admin_add_incoming.sender_details, JSON_REJECT_DUPLICATES, NULL); if (NULL == sender_details) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse sender details `%s' at %u\n", cmd->details.admin_add_incoming.sender_details, is->ip); fail (is); return; } transfer_details = json_loads (cmd->details.admin_add_incoming.transfer_details, JSON_REJECT_DUPLICATES, NULL); if (NULL == transfer_details) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse transfer details `%s' at %u\n", cmd->details.admin_add_incoming.transfer_details, is->ip); fail (is); return; } cmd->details.admin_add_incoming.aih = TALER_EXCHANGE_admin_add_incoming (exchange, "http://localhost:18080/", &reserve_pub, &amount, execution_date, sender_details, transfer_details, &add_incoming_cb, is); json_decref (sender_details); json_decref (transfer_details); if (NULL == cmd->details.admin_add_incoming.aih) { GNUNET_break (0); fail (is); return; } return; case OC_WITHDRAW_STATUS: GNUNET_assert (NULL != cmd->details.reserve_status.reserve_reference); ref = find_command (is, cmd->details.reserve_status.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); GNUNET_CRYPTO_eddsa_key_get_public (&ref->details.admin_add_incoming.reserve_priv.eddsa_priv, &reserve_pub.eddsa_pub); cmd->details.reserve_status.wsh = TALER_EXCHANGE_reserve_status (exchange, &reserve_pub, &reserve_status_cb, is); return; case OC_WITHDRAW_SIGN: GNUNET_assert (NULL != cmd->details.reserve_withdraw.reserve_reference); ref = find_command (is, cmd->details.reserve_withdraw.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); if (NULL != cmd->details.reserve_withdraw.amount) { if (GNUNET_OK != TALER_string_to_amount (cmd->details.reserve_withdraw.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } cmd->details.reserve_withdraw.pk = find_pk (is->keys, &amount); } if (NULL == cmd->details.reserve_withdraw.pk) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to determine denomination key at %u\n", is->ip); fail (is); return; } /* create coin's private key */ { struct GNUNET_CRYPTO_EddsaPrivateKey *priv; priv = GNUNET_CRYPTO_eddsa_key_create (); cmd->details.reserve_withdraw.coin_priv.eddsa_priv = *priv; GNUNET_free (priv); } GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.reserve_withdraw.coin_priv.eddsa_priv, &coin_pub.eddsa_pub); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &cmd->details.reserve_withdraw.blinding_key, sizeof (cmd->details.reserve_withdraw.blinding_key)); cmd->details.reserve_withdraw.wsh = TALER_EXCHANGE_reserve_withdraw (exchange, cmd->details.reserve_withdraw.pk, &ref->details.admin_add_incoming.reserve_priv, &cmd->details.reserve_withdraw.coin_priv, &cmd->details.reserve_withdraw.blinding_key, &reserve_withdraw_cb, is); if (NULL == cmd->details.reserve_withdraw.wsh) { GNUNET_break (0); fail (is); return; } return; case OC_PROPOSAL: { json_t *order; json_error_t error; order = json_loads (cmd->details.proposal.order, JSON_REJECT_DUPLICATES, &error); if (NULL != instance) { json_t *merchant; merchant = json_object (); json_object_set_new (merchant, "instance", json_string (instance)); json_object_set (order, "merchant", merchant); } if (NULL == order) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse the order `%s' at command #%u: %s at %u\n", cmd->details.proposal.order, is->ip, error.text, (unsigned int) error.column); fail (is); return; } cmd->details.proposal.po = TALER_MERCHANT_order_put (ctx, MERCHANT_URI, order, &proposal_cb, is); json_decref (order); if (NULL == cmd->details.proposal.po) { GNUNET_break (0); fail (is); return; } return; } case OC_PAY: { struct TALER_MERCHANT_PayCoin pc; const char *order_id; struct GNUNET_TIME_Absolute refund_deadline; struct GNUNET_TIME_Absolute pay_deadline; struct GNUNET_TIME_Absolute timestamp; struct GNUNET_HashCode h_wire; struct TALER_MerchantPublicKeyP merchant_pub; struct TALER_MerchantSignatureP merchant_sig; struct TALER_Amount total_amount; struct TALER_Amount max_fee; const char *error_name; unsigned int error_line; /* get proposal */ ref = find_command (is, cmd->details.pay.contract_ref); GNUNET_assert (NULL != ref); merchant_sig = ref->details.proposal.merchant_sig; GNUNET_assert (NULL != ref->details.proposal.proposal_data); { /* Get information that need to be replied in the deposit permission */ struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("order_id", &order_id), GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), GNUNET_JSON_spec_absolute_time ("pay_deadline", &pay_deadline), GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp), 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.proposal.proposal_data, 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; } cmd->details.pay.merchant_pub = merchant_pub; } { const struct Command *coin_ref; memset (&pc, 0, sizeof (pc)); coin_ref = find_command (is, cmd->details.pay.coin_ref); GNUNET_assert (NULL != ref); switch (coin_ref->oc) { case OC_WITHDRAW_SIGN: pc.coin_priv = coin_ref->details.reserve_withdraw.coin_priv; pc.denom_pub = coin_ref->details.reserve_withdraw.pk->key; pc.denom_sig = coin_ref->details.reserve_withdraw.sig; pc.denom_value = coin_ref->details.reserve_withdraw.pk->value; break; default: GNUNET_assert (0); } if (GNUNET_OK != TALER_string_to_amount (cmd->details.pay.amount_without_fee, &pc.amount_without_fee)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.pay.amount_without_fee, is->ip); fail (is); return; } if (GNUNET_OK != TALER_string_to_amount (cmd->details.pay.amount_with_fee, &pc.amount_with_fee)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.pay.amount_with_fee, is->ip); fail (is); return; } } cmd->details.pay.ph = TALER_MERCHANT_pay_wallet (ctx, MERCHANT_URI, instance, &ref->details.proposal.hash, &total_amount, &max_fee, &merchant_pub, &merchant_sig, timestamp, refund_deadline, pay_deadline, &h_wire, EXCHANGE_URI, order_id, 1 /* num_coins */, &pc /* coins */, &pay_cb, is); } if (NULL == cmd->details.pay.ph) { GNUNET_break (0); fail (is); return; } return; case OC_RUN_AGGREGATOR: { const struct GNUNET_DISK_FileHandle *pr; cmd->details.run_aggregator.aggregator_proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-aggregator", "taler-exchange-aggregator", "-c", "test_merchant_api.conf", "-t", /* exit when done */ NULL); if (NULL == cmd->details.run_aggregator.aggregator_proc) { GNUNET_break (0); fail (is); return; } pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); cmd->details.run_aggregator.child_death_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, pr, &maint_child_death, is); return; } case OC_CHECK_BANK_TRANSFER: { if (GNUNET_OK != TALER_string_to_amount (cmd->details.check_bank_transfer.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } if (GNUNET_OK != TALER_FAKEBANK_check (fakebank, &amount, cmd->details.check_bank_transfer.account_debit, cmd->details.check_bank_transfer.account_credit, &cmd->details.check_bank_transfer.wtid)) { GNUNET_break (0); fail (is); return; } next_command (is); return; } case OC_CHECK_BANK_TRANSFERS_EMPTY: { if (GNUNET_OK != TALER_FAKEBANK_check_empty (fakebank)) { GNUNET_break (0); fail (is); return; } next_command (is); return; } case OC_TRACK_TRANSFER: ref = find_command (is, cmd->details.track_transfer.check_bank_ref); GNUNET_assert (NULL != ref); cmd->details.track_transfer.tdo = TALER_MERCHANT_track_transfer (ctx, MERCHANT_URI, instance, &ref->details.check_bank_transfer.wtid, EXCHANGE_URI, &track_transfer_cb, is); return; case OC_TRACK_TRANSACTION: { const struct Command *proposal_ref; const char *order_id; ref = find_command (is, cmd->details.track_transaction.pay_ref); GNUNET_assert (NULL != ref); proposal_ref = find_command (is, ref->details.pay.contract_ref); order_id = json_string_value (json_object_get (proposal_ref->details.proposal.proposal_data, "order_id")); cmd->details.track_transaction.tth = TALER_MERCHANT_track_transaction (ctx, MERCHANT_URI, instance, order_id, &track_transaction_cb, is); } return; case OC_HISTORY: if (NULL == (cmd->details.history.ho = TALER_MERCHANT_history (ctx, MERCHANT_URI, instance, cmd->details.history.date, history_cb, is))) { fail (is); return; } break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown instruction %d at %u (%s)\n", cmd->oc, is->ip, cmd->label); fail (is); return; } } /** * Function run when the test times out. * * @param cls NULL */ static void do_timeout (void *cls) { timeout_task = NULL; GNUNET_SCHEDULER_shutdown (); } /** * Function run when the test terminates (good or bad). * Cleans up our state. * * @param cls the interpreter state. */ static void do_shutdown (void *cls) { struct InterpreterState *is = cls; struct Command *cmd; unsigned int i; if (NULL != timeout_task) { GNUNET_SCHEDULER_cancel (timeout_task); timeout_task = NULL; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Shutdown executing\n"); for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) { switch (cmd->oc) { case OC_END: GNUNET_assert (0); break; case OC_PROPOSAL_LOOKUP: if (NULL != cmd->details.proposal_lookup.plo) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_MERCHANT_proposal_lookup_cancel (cmd->details.proposal_lookup.plo); } break; case OC_ADMIN_ADD_INCOMING: if (NULL != cmd->details.admin_add_incoming.aih) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih); cmd->details.admin_add_incoming.aih = NULL; } break; case OC_WITHDRAW_STATUS: if (NULL != cmd->details.reserve_status.wsh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_reserve_status_cancel (cmd->details.reserve_status.wsh); cmd->details.reserve_status.wsh = NULL; } break; case OC_WITHDRAW_SIGN: if (NULL != cmd->details.reserve_withdraw.wsh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_reserve_withdraw_cancel (cmd->details.reserve_withdraw.wsh); cmd->details.reserve_withdraw.wsh = NULL; } if (NULL != cmd->details.reserve_withdraw.sig.rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (cmd->details.reserve_withdraw.sig.rsa_signature); cmd->details.reserve_withdraw.sig.rsa_signature = NULL; } break; case OC_PROPOSAL: if (NULL != cmd->details.proposal.po) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_MERCHANT_proposal_cancel (cmd->details.proposal.po); cmd->details.proposal.po = NULL; } if (NULL != cmd->details.proposal.proposal_data) { json_decref (cmd->details.proposal.proposal_data); cmd->details.proposal.proposal_data = NULL; } break; case OC_PAY: if (NULL != cmd->details.pay.ph) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_MERCHANT_pay_cancel (cmd->details.pay.ph); cmd->details.pay.ph = NULL; } break; case OC_RUN_AGGREGATOR: if (NULL != cmd->details.run_aggregator.aggregator_proc) { GNUNET_break (0 == GNUNET_OS_process_kill (cmd->details.run_aggregator.aggregator_proc, SIGKILL)); GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc); GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc); cmd->details.run_aggregator.aggregator_proc = NULL; } if (NULL != cmd->details.run_aggregator.child_death_task) { GNUNET_SCHEDULER_cancel (cmd->details.run_aggregator.child_death_task); cmd->details.run_aggregator.child_death_task = NULL; } break; case OC_CHECK_BANK_TRANSFER: break; case OC_CHECK_BANK_TRANSFERS_EMPTY: break; case OC_TRACK_TRANSFER: if (NULL != cmd->details.track_transfer.tdo) { TALER_MERCHANT_track_transfer_cancel (cmd->details.track_transfer.tdo); cmd->details.track_transfer.tdo = NULL; } break; case OC_TRACK_TRANSACTION: if (NULL != cmd->details.track_transaction.tth) { TALER_MERCHANT_track_transaction_cancel (cmd->details.track_transaction.tth); cmd->details.track_transaction.tth = NULL; } break; case OC_HISTORY: if (NULL != cmd->details.history.ho) { TALER_MERCHANT_history_cancel (cmd->details.history.ho); cmd->details.history.ho = NULL; } break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Shutdown: unknown instruction %d at %u (%s)\n", cmd->oc, i, cmd->label); break; } } if (NULL != is->task) { GNUNET_SCHEDULER_cancel (is->task); is->task = NULL; } GNUNET_free (is); GNUNET_free_non_null (instances); if (NULL != exchange) { TALER_EXCHANGE_disconnect (exchange); exchange = NULL; } if (NULL != ctx) { GNUNET_CURL_fini (ctx); ctx = NULL; } if (NULL != rc) { GNUNET_CURL_gnunet_rc_destroy (rc); rc = NULL; } TALER_FAKEBANK_stop (fakebank); fakebank = NULL; db->drop_tables (db->cls); TALER_MERCHANTDB_plugin_unload (db); GNUNET_CONFIGURATION_destroy (cfg); } /** * Functions of this type are called to provide the retrieved signing and * denomination keys of the exchange. No TALER_EXCHANGE_*() functions should be called * in this callback. * * @param cls closure * @param keys information about keys of the exchange */ static void cert_cb (void *cls, const struct TALER_EXCHANGE_Keys *keys) { struct InterpreterState *is = cls; /* check that keys is OK */ #define ERR(cond) do { if(!(cond)) break; GNUNET_break (0); GNUNET_SCHEDULER_shutdown(); return; } while (0) ERR (NULL == keys); ERR (0 == keys->num_sign_keys); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Read %u signing keys\n", keys->num_sign_keys); ERR (0 == keys->num_denom_keys); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Read %u denomination keys\n", keys->num_denom_keys); #undef ERR /* run actual tests via interpreter-loop */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Certificate callback invoked, starting interpreter\n"); is->keys = keys; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); } /** * Signal handler called for SIGCHLD. Triggers the * respective handler by writing to the trigger pipe. */ static void sighandler_child_death () { static char c; int old_errno = errno; /* back-up errno */ GNUNET_break (1 == GNUNET_DISK_file_write (GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_WRITE), &c, sizeof (c))); errno = old_errno; /* restore errno */ } /** * Main function that will be run by the scheduler. * * @param cls closure */ static void run (void *cls) { struct InterpreterState *is; static struct Command commands[] = { /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-1", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":62, \"uuid\":1 }", .details.admin_add_incoming.transfer_details = "{ \"uuid\": 1}", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "create-reserve-1", .details.reserve_withdraw.amount = "EUR:5" }, /* Check that deposit and withdraw operation are in history, and that the balance is now at zero */ { .oc = OC_WITHDRAW_STATUS, .label = "withdraw-status-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_status.reserve_reference = "create-reserve-1", .details.reserve_status.expected_balance = "EUR:0" }, /* Create proposal */ { .oc = OC_PROPOSAL, .label = "create-proposal-1", .expected_response_code = MHD_HTTP_OK, .details.proposal.order = "{\ \"max_fee\":\ {\"currency\":\"EUR\", \"value\":0, \"fraction\":50000000},\ \"order_id\":\"1\",\ \"timestamp\":\"\\/Date(42)\\/\",\ \"refund_deadline\":\"\\/Date(0)\\/\",\ \"pay_deadline\":\"\\/Date(9999999999)\\/\",\ \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0},\ \"summary\": \"merchant-lib testcase\",\ \"products\":\ [ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }"}, { .oc = OC_PAY, .label = "deposit-simple", .expected_response_code = MHD_HTTP_OK, .details.pay.contract_ref = "create-proposal-1", .details.pay.coin_ref = "withdraw-coin-1", .details.pay.amount_with_fee = "EUR:5", .details.pay.amount_without_fee = "EUR:4.99" }, /* Try to replay payment reusing coin */ { .oc = OC_PAY, .label = "replay-simple", .expected_response_code = MHD_HTTP_OK, .details.pay.contract_ref = "create-proposal-1", .details.pay.coin_ref = "withdraw-coin-1", .details.pay.amount_with_fee = "EUR:5", .details.pay.amount_without_fee = "EUR:4.99" }, /* Create another contract */ { .oc = OC_PROPOSAL, .label = "create-proposal-2", .expected_response_code = MHD_HTTP_OK, .details.proposal.order = "{\ \"max_fee\":\ {\"currency\":\"EUR\", \"value\":0, \"fraction\":50000000},\ \"order_id\":\"2\",\ \"timestamp\":\"\\/Date(42)\\/\",\ \"refund_deadline\":\"\\/Date(0)\\/\",\ \"pay_deadline\":\"\\/Date(9999999999)\\/\",\ \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0},\ \"products\":\ [ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }" }, /* 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.contract_ref = "create-proposal-2", .details.pay.coin_ref = "withdraw-coin-1", .details.pay.amount_with_fee = "EUR:5", .details.pay.amount_without_fee = "EUR:4.99" }, /* Fill second reserve with EUR:1 */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-2", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":63, \"uuid\":2 }", .details.admin_add_incoming.transfer_details = "{ \"uuid\": 2}", .details.admin_add_incoming.amount = "EUR:1" }, /* Add another 4.01 EUR to reserve #2 */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-2b", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.reserve_reference = "create-reserve-2", .details.admin_add_incoming.sender_details = "{ \"type\":\"test\", \"bank_uri\":\"" BANK_URI "\", \"account_number\":63, \"uuid\":3 }", .details.admin_add_incoming.transfer_details = "{ \"uuid\": 3}", .details.admin_add_incoming.amount = "EUR:4.01" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-2", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "create-reserve-2", .details.reserve_withdraw.amount = "EUR:5" }, /* Proposal lookup */ { .oc = OC_PROPOSAL_LOOKUP, .label = "fetch-proposal-2", .expected_response_code = MHD_HTTP_OK, .details.proposal_lookup.proposal_reference = "create-proposal-2" }, /* Check nothing happened on the bank side so far */ { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check_bank_empty" }, /* Run transfers. */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator" }, /* Obtain WTID of the transfer */ { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-499c", .details.check_bank_transfer.amount = "EUR:4.99", .details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */ .details.check_bank_transfer.account_credit = 62 /* merchant */ }, /* Check that there are no other unusual transfers */ { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check_bank_empty" }, { .oc = OC_TRACK_TRANSACTION, .label = "track-transaction-1", .expected_response_code = MHD_HTTP_OK, .details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c", .details.track_transaction.pay_ref = "deposit-simple" }, /* Trace the WTID back to the original transaction */ { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-1", .expected_response_code = MHD_HTTP_OK, .details.track_transfer.check_bank_ref = "check_bank_transfer-499c", .details.track_transfer.expected_pay_ref = "deposit-simple" }, { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-1-again", .expected_response_code = MHD_HTTP_OK, .details.track_transfer.check_bank_ref = "check_bank_transfer-499c", .details.track_transfer.expected_pay_ref = "deposit-simple" }, /* Pay again successfully on 2nd contract */ { .oc = OC_PAY, .label = "deposit-simple-2", .expected_response_code = MHD_HTTP_OK, .details.pay.contract_ref = "create-proposal-2", .details.pay.coin_ref = "withdraw-coin-2", .details.pay.amount_with_fee = "EUR:5", .details.pay.amount_without_fee = "EUR:4.99" }, /* Run transfers. */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator-2" }, /* Obtain WTID of the transfer */ { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-499c-2", .details.check_bank_transfer.amount = "EUR:4.99", .details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */ .details.check_bank_transfer.account_credit = 62 /* merchant */ }, /* Check that there are no other unusual transfers */ { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check_bank_empty" }, /* Trace the WTID back to the original transaction */ { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-2", .expected_response_code = MHD_HTTP_OK, .details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2", .details.track_transfer.expected_pay_ref = "deposit-simple-2" }, { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-2-again", .expected_response_code = MHD_HTTP_OK, .details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2", .details.track_transfer.expected_pay_ref = "deposit-simple-2" }, { .oc = OC_TRACK_TRANSACTION, .label = "track-transaction-2", .expected_response_code = MHD_HTTP_OK, .details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c-2", .details.track_transaction.pay_ref = "deposit-simple-2" }, { .oc = OC_HISTORY, .label = "history-1", .expected_response_code = MHD_HTTP_OK, /*all records to be returned*/ .details.history.date.abs_value_us = 0, .details.history.nresult = 2 }, { .oc = OC_HISTORY, .label = "history-2", .expected_response_code = MHD_HTTP_OK, /*no records to be returned, as limit is in the future*/ .details.history.date.abs_value_us = 43 * 1000LL * 1000LL, .details.history.nresult = 0 }, /* end of testcase */ { .oc = OC_END } }; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Interpreter initializing\n"); fakebank = TALER_FAKEBANK_start (BANK_PORT); if (NULL == fakebank) { fprintf (stderr, "\nFailed to start fake bank service\n"); result = 77; return; } is = GNUNET_new (struct InterpreterState); is->commands = commands; ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); GNUNET_assert (NULL != ctx); rc = GNUNET_CURL_gnunet_rc_create (ctx); exchange = TALER_EXCHANGE_connect (ctx, EXCHANGE_URI, &cert_cb, is, TALER_EXCHANGE_OPTION_END); GNUNET_assert (NULL != exchange); timeout_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 150), &do_timeout, NULL); GNUNET_SCHEDULER_add_shutdown (&do_shutdown, is); } /** * Main function for the testcase for the exchange API. * * @param argc expected to be 1 * @param argv expected to only contain the program name */ int main (int argc, char * const *argv) { char *_instances; char *token; struct GNUNET_OS_Process *proc; struct GNUNET_OS_Process *exchanged; struct GNUNET_OS_Process *merchantd; unsigned int cnt; struct GNUNET_SIGNAL_Context *shc_chld; unsetenv ("XDG_DATA_HOME"); unsetenv ("XDG_CONFIG_HOME"); GNUNET_log_setup ("test-merchant-api", "DEBUG", NULL); cfg = GNUNET_CONFIGURATION_create (); GNUNET_assert (GNUNET_OK == GNUNET_CONFIGURATION_load (cfg, "test_merchant_api.conf")); GNUNET_break (GNUNET_CONFIGURATION_get_value_string (cfg, "merchant", "INSTANCES", &_instances)); GNUNET_break (NULL != (token = strtok (_instances, " "))); GNUNET_array_append (instances, ninstances, token); while (NULL != (token = strtok (NULL, " "))) GNUNET_array_append(instances, ninstances, token); instance = instances[instance_idx]; 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; } proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-keyup", "taler-exchange-keyup", "-c", "test_merchant_api.conf", NULL); if (NULL == proc) { fprintf (stderr, "Failed to run taler-exchange-keyup. Check your PATH.\n"); return 77; } GNUNET_OS_process_wait (proc); GNUNET_OS_process_destroy (proc); proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-dbinit", "taler-exchange-dbinit", "-c", "test_merchant_api.conf", "-r", NULL); if (NULL == proc) { fprintf (stderr, "Failed to run taler-exchange-dbinit. Check your PATH.\n"); return 77; } GNUNET_OS_process_wait (proc); GNUNET_OS_process_destroy (proc); exchanged = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-httpd", "taler-exchange-httpd", "-c", "test_merchant_api.conf", NULL); if (NULL == exchanged) { fprintf (stderr, "Failed to run taler-exchange-httpd. Check your PATH.\n"); return 77; } /* give child time to start and bind against the socket */ fprintf (stderr, "Waiting for taler-exchange-httpd to be ready\n"); cnt = 0; do { fprintf (stderr, "."); sleep (1); cnt++; if (cnt > 60) { fprintf (stderr, "\nFailed to start taler-exchange-httpd\n"); GNUNET_OS_process_kill (exchanged, SIGKILL); GNUNET_OS_process_wait (exchanged); GNUNET_OS_process_destroy (exchanged); return 77; } } while (0 != system ("wget -q -t 1 -T 1 " EXCHANGE_URI "keys -o /dev/null -O /dev/null")); fprintf (stderr, "\n"); merchantd = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "valgrind", "valgrind", "taler-merchant-httpd", "-c", "test_merchant_api.conf", "-L", "DEBUG", NULL); if (NULL == merchantd) { fprintf (stderr, "Failed to run taler-merchant-httpd. Check your PATH.\n"); GNUNET_OS_process_kill (exchanged, SIGKILL); GNUNET_OS_process_wait (exchanged); GNUNET_OS_process_destroy (exchanged); return 77; } /* give child time to start and bind against the socket */ fprintf (stderr, "Waiting for taler-merchant-httpd to be ready\n"); cnt = 0; do { fprintf (stderr, "."); sleep (1); cnt++; if (cnt > 60) { fprintf (stderr, "\nFailed to start taler-merchant-httpd\n"); GNUNET_OS_process_kill (merchantd, SIGKILL); GNUNET_OS_process_wait (merchantd); GNUNET_OS_process_destroy (merchantd); GNUNET_OS_process_kill (exchanged, SIGKILL); GNUNET_OS_process_wait (exchanged); GNUNET_OS_process_destroy (exchanged); return 77; } } while (0 != system ("wget -q -t 1 -T 1 " MERCHANT_URI " -o /dev/null -O /dev/null")); fprintf (stderr, "\n"); result = GNUNET_SYSERR; sigpipe = GNUNET_DISK_pipe (GNUNET_NO, GNUNET_NO, GNUNET_NO, GNUNET_NO); GNUNET_assert (NULL != sigpipe); shc_chld = GNUNET_SIGNAL_handler_install (GNUNET_SIGCHLD, &sighandler_child_death); GNUNET_SCHEDULER_run (&run, NULL); GNUNET_SIGNAL_handler_uninstall (shc_chld); shc_chld = NULL; GNUNET_DISK_pipe_close (sigpipe); GNUNET_OS_process_kill (merchantd, SIGTERM); GNUNET_OS_process_wait (merchantd); GNUNET_OS_process_destroy (merchantd); GNUNET_OS_process_kill (exchanged, SIGTERM); GNUNET_OS_process_wait (exchanged); GNUNET_OS_process_destroy (exchanged); if (77 == result) return 77; return (GNUNET_OK == result) ? 0 : 1; }