/* This file is part of TALER (C) 2017-2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file backend/taler-merchant-httpd_post-tips-ID-pickup.c * @brief implementation of a POST /tips/ID/pickup handler * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_post-tips-ID-pickup.h" /** * How often do we retry on serialization errors? */ #define MAX_RETRIES 3 /** * How long do we give the exchange operation to complete withdrawing * all of the planchets? */ #define EXCHANGE_TIMEOUT GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 45) /** * Active pickup operations. */ struct PickupContext; /** * Handle for an individual planchet we are processing for a tip. */ struct PlanchetOperation { /** * Active pickup operation this planchet belongs with. */ struct PickupContext *pc; /** * Kept in a DLL. */ struct PlanchetOperation *prev; /** * Kept in a DLL. */ struct PlanchetOperation *next; /** * Find operation (while active), later NULL. */ struct TMH_EXCHANGES_FindOperation *fo; /** * Withdraw handle (NULL while @e fo is active). */ struct TALER_EXCHANGE_Withdraw2Handle *w2h; /** * Details about the planchet for withdrawing. */ struct TALER_PlanchetDetail pd; /** * Offset of this planchet in the original request. */ unsigned int offset; }; /** * Active pickup operations. */ struct PickupContext { /** * Kept in a DLL. */ struct PickupContext *next; /** * Kept in a DLL. */ struct PickupContext *prev; /** * The connection. */ struct MHD_Connection *connection; /** * Timeout task. */ struct GNUNET_SCHEDULER_Task *tt; /** * Head of DLL of exchange operations on planchets. */ struct PlanchetOperation *po_head; /** * Tail of DLL of exchange operations on planchets. */ struct PlanchetOperation *po_tail; /** * HTTP response to return (set on errors). */ struct MHD_Response *response; /** * Find operation (while active), later NULL. */ struct TMH_EXCHANGES_FindOperation *fo; /** * Which reserve are we draining? */ struct TALER_ReservePrivateKeyP reserve_priv; /** * Which tip is being picked up? */ struct GNUNET_HashCode tip_id; /** * What is the ID of the pickup operation? (Basically a * hash over the key inputs). */ struct GNUNET_HashCode pickup_id; /** * Array of our planchets. */ struct TALER_PlanchetDetail *planchets; /** * Length of the @e planchets array. */ unsigned int planchets_length; /** * HTTP status to use (set on errors). */ unsigned int http_status; /** * Total amount requested in the pick up operation. Computed by * totaling up the amounts of all the @e planchets. */ struct TALER_Amount total_requested; /** * True if @e total_requested has been initialized. */ bool tr_initialized; }; /** * Head of DLL. */ static struct PickupContext *pc_head; /** * Tail of DLL. */ static struct PickupContext *pc_tail; /** * Stop all ongoing operations associated with @a pc. */ static void stop_operations (struct PickupContext *pc) { struct PlanchetOperation *po; if (NULL != pc->tt) { GNUNET_SCHEDULER_cancel (pc->tt); pc->tt = NULL; } if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } while (NULL != (po = pc->po_head)) { if (NULL != po->fo) { TMH_EXCHANGES_find_exchange_cancel (po->fo); po->fo = NULL; } if (NULL != po->w2h) { TALER_EXCHANGE_withdraw2_cancel (po->w2h); po->w2h = NULL; } GNUNET_CONTAINER_DLL_remove (pc->po_head, pc->po_tail, po); } } /** * Function called to clean up. * * @param cls a `struct PickupContext *` to clean up */ static void pick_context_cleanup (void *cls) { struct PickupContext *pc = cls; stop_operations (pc); /* should not be any... */ for (unsigned int i = 0; iplanchets_length; i++) GNUNET_free (pc->planchets[i].coin_ev); GNUNET_array_grow (pc->planchets, pc->planchets_length, 0); GNUNET_free (pc); } /** * We are shutting down, force resuming all suspended pickup operations. */ void TMH_force_tip_pickup_resume () { for (struct PickupContext *pc = pc_head; NULL != pc; pc = pc->next) { stop_operations (pc); MHD_resume_connection (pc->connection); } } /** * Callbacks of this type are used to serve the result of submitting a * withdraw request to a exchange without the (un)blinding factor. * We persist the result in the database and, if we were the last * planchet operation, resume HTTP processing. * * @param cls closure with a `struct PlanchetOperation *` * @param hr HTTP response data * @param blind_sig blind signature over the coin, NULL on error */ static void withdraw_cb (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, const struct GNUNET_CRYPTO_RsaSignature *blind_sig) { struct PlanchetOperation *po = cls; struct PickupContext *pc = po->pc; enum GNUNET_DB_QueryStatus qs; GNUNET_CONTAINER_DLL_remove (pc->po_head, pc->po_tail, po); if (NULL == blind_sig) { stop_operations (pc); pc->http_status = MHD_HTTP_FAILED_DEPENDENCY; pc->response = TALER_MHD_make_json_pack ( "{s:I, s:I, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_PICKUP_EXCHANGE_ERROR, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "exchange_reply", hr->reply); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } qs = TMH_db->insert_pickup_blind_signature (TMH_db->cls, &pc->pickup_id, po->offset, blind_sig); if (qs < 0) { stop_operations (pc); pc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; pc->response = TALER_MHD_make_error ( TALER_EC_TIP_PICKUP_DB_STORE_HARD_ERROR, "Could not store blind signature in DB"); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } if (NULL == pc->po_head) { stop_operations (pc); /* stops timeout job */ MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation as part of a withdraw objective. If the exchange is ready, * withdraws the planchet from the exchange. * * @param cls closure, with our `struct PlanchetOperation *` * @param hr HTTP response details * @param eh handle to the exchange context * @param payto_uri payto://-URI of the exchange * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted true if this exchange is trusted by config */ static void do_withdraw (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct PlanchetOperation *po = cls; struct PickupContext *pc = po->pc; po->fo = NULL; if (NULL == eh) { stop_operations (pc); GNUNET_CONTAINER_DLL_remove (pc->po_head, pc->po_tail, po); pc->http_status = MHD_HTTP_FAILED_DEPENDENCY; pc->response = TALER_MHD_make_json_pack ( "{s:I, s:I, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_PICKUP_CONTACT_EXCHANGE_ERROR, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "exchange_reply", hr->reply); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } po->w2h = TALER_EXCHANGE_withdraw2 (eh, &po->pd, &pc->reserve_priv, &withdraw_cb, po); } /** * Withdraw @a planchet from @a exchange_url for @a pc operation at planchet * @a offset. Sets up the respective operation and adds it @a pc's operation * list. Once the operation is complete, the resulting blind signature is * committed to the merchant's database. If all planchet operations are * completed, the HTTP processing is resumed. * * @param[in,out] pc a pending pickup operation that includes @a planchet * @param exchange_url identifies an exchange to do the pickup from * @param planchet details about the coin to pick up * @param offset offset of @a planchet in the list, needed to process the reply */ static void try_withdraw (struct PickupContext *pc, const char *exchange_url, const struct TALER_PlanchetDetail *planchet, unsigned int offset) { struct PlanchetOperation *po; po = GNUNET_new (struct PlanchetOperation); po->pc = pc; po->pd = *planchet; po->offset = offset; po->fo = TMH_EXCHANGES_find_exchange (exchange_url, NULL, GNUNET_NO, &do_withdraw, po); GNUNET_assert (NULL != po->fo); GNUNET_CONTAINER_DLL_insert (pc->po_head, pc->po_tail, po); } /** * Handle timeout for pickup. * * @param cls a `struct PickupContext *` */ static void do_timeout (void *cls) { struct PickupContext *pc = cls; pc->tt = NULL; stop_operations (pc); pc->http_status = MHD_HTTP_REQUEST_TIMEOUT; pc->response = TALER_MHD_make_error ( TALER_EC_TIP_PICKUP_EXCHANGE_TIMEOUT, "Timeout trying to withdraw from exchange (try again later)"); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation as part of a withdraw objective. Here, we initialize * the "total_requested" amount by adding up the cost of the planchets * provided by the client. * * @param cls closure, with our `struct PickupContext *` * @param hr HTTP response details * @param eh handle to the exchange context * @param payto_uri payto://-URI of the exchange * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted true if this exchange is trusted by config */ static void compute_total_requested (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct PickupContext *pc = cls; const struct TALER_EXCHANGE_Keys *keys; pc->fo = NULL; stop_operations (pc); /* stops timeout job */ if ( (NULL == eh) || (NULL == (keys = TALER_EXCHANGE_get_keys (eh))) ) { pc->http_status = MHD_HTTP_FAILED_DEPENDENCY; pc->response = TALER_MHD_make_json_pack ( "{s:I, s:I, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_PICKUP_EXCHANGE_KEYS_ERROR, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "exchange_reply", hr->reply); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } TALER_amount_get_zero (TMH_currency, &pc->total_requested); for (unsigned int i = 0; iplanchets_length; i++) { struct TALER_PlanchetDetail *pd = &pc->planchets[i]; const struct TALER_EXCHANGE_DenomPublicKey *dpk; dpk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, &pd->denom_pub_hash); if (NULL == dpk) { pc->http_status = MHD_HTTP_CONFLICT; pc->response = TALER_MHD_make_json_pack ( "{s:I, s:I, s:I, s:O}", "code", (json_int_t) TALER_EC_TIP_PICKUP_DENOMINATION_UNKNOWN, "exchange_code", (json_int_t) hr->ec, "exchange_http_status", (json_int_t) hr->http_status, "exchange_reply", hr->reply); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } if ( (GNUNET_YES != TALER_amount_cmp_currency (&pc->total_requested, &dpk->value)) || (0 > TALER_amount_add (&pc->total_requested, &pc->total_requested, &dpk->value)) ) { pc->http_status = MHD_HTTP_BAD_REQUEST; pc->response = TALER_MHD_make_error (TALER_EC_TIP_PICKUP_SUMMATION_FAILED, "Could not add up values to compute pickup total"); MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ return; } } pc->tr_initialized = true; MHD_resume_connection (pc->connection); TMH_trigger_daemon (); /* we resumed, kick MHD */ } /** * The tip lookup operation failed. Generate an error response based on the @a qs. * * @param connection connection to generate error for * @param qs DB status to base error creation on * @return MHD result code */ static MHD_RESULT reply_lookup_tip_failed (struct MHD_Connection *connection, enum GNUNET_DB_QueryStatus qs) { unsigned int response_code; enum TALER_ErrorCode ec; TMH_db->rollback (TMH_db->cls); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: ec = TALER_EC_TIP_PICKUP_TIP_ID_UNKNOWN; response_code = MHD_HTTP_NOT_FOUND; break; case GNUNET_DB_STATUS_SOFT_ERROR: ec = TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; break; case GNUNET_DB_STATUS_HARD_ERROR: ec = TALER_EC_TIP_PICKUP_DB_ERROR_HARD; response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; break; default: GNUNET_break (0); ec = TALER_EC_INTERNAL_LOGIC_ERROR; response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; break; } return TALER_MHD_reply_with_error (connection, response_code, ec, "Could not process pickup"); } /** * Manages a POST /tips/$ID/pickup call, checking that the tip is authorized, * and if so, returning the blind signatures. * * @param rh context of the handler * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ MHD_RESULT TMH_post_tips_ID_pickup (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct PickupContext *pc = hc->ctx; char *exchange_url; struct TALER_Amount total_authorized; struct TALER_Amount total_picked_up; struct TALER_Amount total_remaining; struct GNUNET_TIME_Absolute expiration; enum GNUNET_DB_QueryStatus qs; unsigned int num_retries; if (NULL == pc) { json_t *planchets; json_t *planchet; size_t index; pc = GNUNET_new (struct PickupContext); hc->ctx = pc; hc->cc = &pick_context_cleanup; GNUNET_assert (NULL != hc->infix); if (GNUNET_OK != GNUNET_CRYPTO_hash_from_string (hc->infix, &pc->tip_id)) { /* tip_id has wrong encoding */ GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_PARAMETER_MALFORMED, "tip_id malformed"); } { struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_json ("planchets", &planchets), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_OK != res) return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } } if (! json_is_array (planchets)) { GNUNET_break_op (0); json_decref (planchets); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_PATCH_INSTANCES_BAD_PAYTO_URIS, "Invalid bank account information"); } GNUNET_array_grow (pc->planchets, pc->planchets_length, json_array_size (planchets)); json_array_foreach (planchets, index, planchet) { struct TALER_PlanchetDetail *pd = &pc->planchets[index]; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", &pd->denom_pub_hash), GNUNET_JSON_spec_varsize ("coin_ev", (void **) &pd->coin_ev, &pd->coin_ev_size), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, planchet, spec); if (GNUNET_OK != res) { json_decref (planchets); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } } } json_decref (planchets); { struct GNUNET_HashContext *hc; hc = GNUNET_CRYPTO_hash_context_start (); GNUNET_CRYPTO_hash_context_read (hc, &pc->tip_id, sizeof (pc->tip_id)); for (unsigned int i = 0; iplanchets_length; i++) { struct TALER_PlanchetDetail *pd = &pc->planchets[i]; GNUNET_CRYPTO_hash_context_read (hc, &pd->denom_pub_hash, sizeof (pd->denom_pub_hash)); GNUNET_CRYPTO_hash_context_read (hc, pd->coin_ev, pd->coin_ev_size); } GNUNET_CRYPTO_hash_context_finish (hc, &pc->pickup_id); } } if (NULL != pc->response) { MHD_RESULT ret; ret = MHD_queue_response (connection, pc->http_status, pc->response); pc->response = NULL; return ret; } if (! pc->tr_initialized) { qs = TMH_db->lookup_tip (TMH_db->cls, hc->instance->settings.id, &pc->tip_id, &total_authorized, &total_picked_up, &expiration, &exchange_url, &pc->reserve_priv); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) return reply_lookup_tip_failed (connection, qs); MHD_suspend_connection (connection); pc->connection = connection; pc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT, &do_timeout, pc); pc->fo = TMH_EXCHANGES_find_exchange (exchange_url, NULL, GNUNET_NO, &compute_total_requested, pc); return MHD_YES; } TMH_db->preflight (TMH_db->cls); num_retries = 0; RETRY: num_retries++; if (num_retries > MAX_RETRIES) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_TIP_PICKUP_DB_ERROR_SOFT, "Too many DB serialization failures"); } if (GNUNET_OK != TMH_db->start (TMH_db->cls, "pickup tip")) { GNUNET_break (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_TIP_PICKUP_DB_ERROR_HARD, "Could not begin transaction"); } { struct GNUNET_CRYPTO_RsaSignature *sigs[GNUNET_NZL (pc->planchets_length)]; memset (sigs, 0, sizeof (struct GNUNET_CRYPTO_RsaSignature *) * GNUNET_NZL ( pc->planchets_length)); qs = TMH_db->lookup_pickup (TMH_db->cls, hc->instance->settings.id, &pc->tip_id, &pc->pickup_id, &exchange_url, &pc->reserve_priv, pc->planchets_length, sigs); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { for (unsigned int i = 0; i< pc->planchets_length; i++) { if (NULL == sigs[i]) { try_withdraw (pc, exchange_url, &pc->planchets[i], i); qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { MHD_suspend_connection (connection); GNUNET_CONTAINER_DLL_insert (pc_head, pc_tail, pc); pc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT, &do_timeout, pc); TMH_db->rollback (TMH_db->cls); return MHD_YES; } } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) { unsigned int response_code; enum TALER_ErrorCode ec; TMH_db->rollback (TMH_db->cls); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: { json_t *blind_sigs; blind_sigs = json_array (); GNUNET_assert (NULL != blind_sigs); for (unsigned int i = 0; iplanchets_length; i++) { GNUNET_assert (0 == json_array_append_new ( blind_sigs, json_pack ("{s:o}", "blind_sig", GNUNET_JSON_from_rsa_signature ( sigs[i])))); GNUNET_CRYPTO_rsa_signature_free (sigs[i]); } return TALER_MHD_reply_json_pack ( connection, MHD_HTTP_OK, "{s:o}", "blind_sigs", blind_sigs); } break; case GNUNET_DB_STATUS_SOFT_ERROR: goto RETRY; break; case GNUNET_DB_STATUS_HARD_ERROR: ec = TALER_EC_TIP_PICKUP_DB_ERROR_HARD; response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; break; default: GNUNET_break (0); ec = TALER_EC_INTERNAL_LOGIC_ERROR; response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; break; } return TALER_MHD_reply_with_error (connection, response_code, ec, "Could not process pickup"); } } qs = TMH_db->lookup_tip (TMH_db->cls, hc->instance->settings.id, &pc->tip_id, &total_authorized, &total_picked_up, &expiration, &exchange_url, &pc->reserve_priv); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) goto RETRY; if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) return reply_lookup_tip_failed (connection, qs); if (0 == GNUNET_TIME_absolute_get_remaining (expiration).rel_value_us) { TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_GONE, TALER_EC_TIP_PICKUP_HAS_EXPIRED, "Could not process pickup: it is too late"); } if (0 > TALER_amount_subtract (&total_remaining, &total_authorized, &total_picked_up)) { GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_INTERNAL_LOGIC_ERROR, "tip already overdrawn"); } if (0 > TALER_amount_cmp (&total_remaining, &pc->total_requested)) { GNUNET_break (0); TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_TIP_PICKUP_AMOUNT_EXCEEDS_TIP_REMAINING, "requested amount exceeds amount left in tip"); } GNUNET_assert (0 < TALER_amount_add (&total_picked_up, &total_picked_up, &pc->total_requested)); qs = TMH_db->insert_pickup (TMH_db->cls, hc->instance->settings.id, &pc->tip_id, &total_picked_up, &pc->pickup_id, &pc->total_requested); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) goto RETRY; return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_TIP_PICKUP_DB_STORE_HARD_ERROR, "Could not store pickup ID in database"); } qs = TMH_db->commit (TMH_db->cls); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) goto RETRY; return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_TIP_PICKUP_DB_STORE_HARD_ERROR, "Could not commit transaction to database"); } MHD_suspend_connection (connection); pc->tt = GNUNET_SCHEDULER_add_delayed (EXCHANGE_TIMEOUT, &do_timeout, pc); for (unsigned int i = 0; iplanchets_length; i++) { try_withdraw (pc, exchange_url, &pc->planchets[i], i); } return MHD_YES; }