/* This file is part of TALER (C) 2016, 2017, 2018 Inria and GNUnet e.V. 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 bank-lib/fakebank.c * @brief library that fakes being a Taler bank for testcases * @author Christian Grothoff */ #include "platform.h" #include "taler_fakebank_lib.h" #include "taler_bank_service.h" /** * Maximum POST request size (for /admin/add/incoming) */ #define REQUEST_BUFFER_MAX (4*1024) /** * Parse URL arguments of a /history[-range] HTTP request. * * @param connection MHD connection object. * @param ha @a HistoryArgs structure. */ #define PARSE_HISTORY_ARGS(connection, ha) \ parse_history_args_ (connection, ha, __FUNCTION__) /** * Details about a transcation we (as the simulated bank) received. */ struct Transaction { /** * We store transactions in a DLL. */ struct Transaction *next; /** * We store transactions in a DLL. */ struct Transaction *prev; /** * Amount to be transferred. */ struct TALER_Amount amount; /** * Account to debit. */ uint64_t debit_account; /** * Account to credit. */ uint64_t credit_account; /** * Subject of the transfer. */ char *subject; /** * Base URL of the exchange. */ char *exchange_base_url; /** * When did the transaction happen? */ struct GNUNET_TIME_Absolute date; /** * Number of this transaction. */ uint64_t row_id; /** * Flag set if the transfer was rejected. */ int rejected; /** * Has this transaction been subjected to #TALER_FAKEBANK_check() * and should thus no longer be counted in * #TALER_FAKEBANK_check_empty()? */ int checked; }; /** * Needed to implement ascending/descending ordering * of /history results. */ struct HistoryElement { /** * History JSON element. */ json_t *element; /** * Previous element. */ struct HistoryElement *prev; /** * Next element. */ struct HistoryElement *next; }; /** * Values to implement the "/history-range" range. */ struct HistoryRangeDates { /** * Oldest row in the results. */ struct GNUNET_TIME_Absolute start; /** * Youngest row in the results. */ struct GNUNET_TIME_Absolute end; }; /** * Values to implement the "/history" range. */ struct HistoryRangeIds { /** * (Exclusive) row ID for the result set. */ unsigned long long start; /** * How many transactions we want in the result set. If * negative/positive, @a start will be strictly younger/older * of any element in the result set. */ long long count; }; /** * This is the "base" structure for both the /history and the * /history-range API calls. */ struct HistoryArgs { /** * Direction asked by the client: CREDIT / DEBIT / BOTH / CANCEL. */ enum TALER_BANK_Direction direction; /** * Bank account number of the requesting client. */ unsigned long long account_number; /** * Ordering of the results. */ unsigned int ascending; /** * Overloaded type that indicates the "range" to be returned * in the results; this can be either a date range, or a * starting row id + the count. */ void *range; }; /** * Handle for the fake bank. */ struct TALER_FAKEBANK_Handle { /** * We store transactions in a DLL. */ struct Transaction *transactions_head; /** * We store transactions in a DLL. */ struct Transaction *transactions_tail; /** * HTTP server we run to pretend to be the "test" bank. */ struct MHD_Daemon *mhd_bank; /** * Task running HTTP server for the "test" bank. */ struct GNUNET_SCHEDULER_Task *mhd_task; /** * Number of transactions. */ uint64_t serial_counter; #if EPOLL_SUPPORT /** * Boxed @e mhd_fd. */ struct GNUNET_NETWORK_Handle *mhd_rfd; /** * File descriptor to use to wait for MHD. */ int mhd_fd; #endif }; /** * Type for a function that decides whether or not * the history-building loop should iterate once again. * Typically called from inside the 'while' condition. * * @param ha history argument. * @param pos current position. * @return GNUNET_YES if the iteration shuold go on. */ typedef int (*CheckAdvance) (const struct HistoryArgs *ha, struct Transaction *pos); /** * Type for a function that steps over the next element * in the list of all transactions, after the current @a pos * _got_ included in the result. */ typedef struct Transaction * (*Step) (const struct HistoryArgs *ha, const struct Transaction *pos); /* * Type for a function that steps over the next element * in the list of all transactions, after the current @a pos * did _not_ get included in the result. */ typedef struct Transaction * (*Skip) (const struct HistoryArgs *ha, const struct Transaction *pos); /** * Check that the @a want_amount was transferred from * the @a want_debit to the @a want_credit account. If * so, set the @a subject to the transfer identifier. * If not, return #GNUNET_SYSERR. * * @param h bank instance * @param want_amount transfer amount desired * @param want_debit account that should have been debited * @param want_credit account that should have been credited * @param exchange_base_url expected base URL of the exchange * i.e. "https://example.com/"; may include a port * @param[out] subject set to the wire transfer identifier * @return #GNUNET_OK on success */ int TALER_FAKEBANK_check (struct TALER_FAKEBANK_Handle *h, const struct TALER_Amount *want_amount, uint64_t want_debit, uint64_t want_credit, const char *exchange_base_url, char **subject) { for (struct Transaction *t = h->transactions_head; NULL != t; t = t->next) { if ( (want_debit == t->debit_account) && (want_credit == t->credit_account) && (0 == TALER_amount_cmp (want_amount, &t->amount)) && (GNUNET_NO == t->checked) && (0 == strcasecmp (exchange_base_url, t->exchange_base_url)) ) { *subject = GNUNET_strdup (t->subject); t->checked = GNUNET_YES; return GNUNET_OK; } } fprintf (stderr, "Did not find matching transaction!\nI have:\n"); for (struct Transaction *t = h->transactions_head; NULL != t; t = t->next) { if (GNUNET_YES == t->checked) continue; fprintf (stderr, "%llu -> %llu (%s) from %s\n", (unsigned long long) t->debit_account, (unsigned long long) t->credit_account, TALER_amount2s (&t->amount), t->exchange_base_url); } fprintf (stderr, "I wanted:\n%llu -> %llu (%s) from %s\n", (unsigned long long) want_debit, (unsigned long long) want_credit, TALER_amount2s (want_amount), exchange_base_url); return GNUNET_SYSERR; } /** * Tell the fakebank to create another wire transfer. * * @param h fake bank handle * @param debit_account account to debit * @param credit_account account to credit * @param amount amount to transfer * @param subject wire transfer subject to use * @param exchange_base_url exchange URL * @return row_id of the transfer */ uint64_t TALER_FAKEBANK_make_transfer (struct TALER_FAKEBANK_Handle *h, uint64_t debit_account, uint64_t credit_account, const struct TALER_Amount *amount, const char *subject, const char *exchange_base_url) { struct Transaction *t; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Making transfer from %llu to %llu over %s and subject %s\n", (unsigned long long) debit_account, (unsigned long long) credit_account, TALER_amount2s (amount), subject); t = GNUNET_new (struct Transaction); t->debit_account = debit_account; t->credit_account = credit_account; t->amount = *amount; t->exchange_base_url = GNUNET_strdup (exchange_base_url); t->row_id = ++h->serial_counter; t->date = GNUNET_TIME_absolute_get (); t->subject = GNUNET_strdup (subject); GNUNET_TIME_round_abs (&t->date); GNUNET_CONTAINER_DLL_insert_tail (h->transactions_head, h->transactions_tail, t); return t->row_id; } /** * Reject incoming wire transfer to account @a credit_account * as identified by @a rowid. * * @param h fake bank handle * @param rowid identifies transfer to reject * @param credit_account account number of owner of credited account * @return #GNUNET_YES on success, #GNUNET_NO if the wire transfer was not found */ int TALER_FAKEBANK_reject_transfer (struct TALER_FAKEBANK_Handle *h, uint64_t rowid, uint64_t credit_account) { for (struct Transaction *t = h->transactions_head; NULL != t; t = t->next) if ( (t->row_id == rowid) && (t->credit_account == credit_account) ) { t->rejected = GNUNET_YES; return GNUNET_YES; } return GNUNET_NO; } /** * Check that no wire transfers were ordered (or at least none * that have not been taken care of via #TALER_FAKEBANK_check()). * If any transactions are onrecord, return #GNUNET_SYSERR. * * @param h bank instance * @return #GNUNET_OK on success */ int TALER_FAKEBANK_check_empty (struct TALER_FAKEBANK_Handle *h) { struct Transaction *t; t = h->transactions_head; while (NULL != t) { if ( (GNUNET_YES != t->checked) && (GNUNET_YES != t->rejected) ) break; t = t->next; } if (NULL == t) return GNUNET_OK; fprintf (stderr, "Expected empty transaction set, but I have:\n"); while (NULL != t) { if ( (GNUNET_YES != t->checked) && (GNUNET_YES != t->rejected) ) { char *s; s = TALER_amount_to_string (&t->amount); fprintf (stderr, "%llu -> %llu (%s) from %s\n", (unsigned long long) t->debit_account, (unsigned long long) t->credit_account, s, t->exchange_base_url); GNUNET_free (s); } t = t->next; } return GNUNET_SYSERR; } /** * Stop running the fake bank. * * @param h bank to stop */ void TALER_FAKEBANK_stop (struct TALER_FAKEBANK_Handle *h) { struct Transaction *t; while (NULL != (t = h->transactions_head)) { GNUNET_CONTAINER_DLL_remove (h->transactions_head, h->transactions_tail, t); GNUNET_free (t->subject); GNUNET_free (t->exchange_base_url); GNUNET_free (t); } if (NULL != h->mhd_task) { GNUNET_SCHEDULER_cancel (h->mhd_task); h->mhd_task = NULL; } #if EPOLL_SUPPORT GNUNET_NETWORK_socket_free_memory_only_ (h->mhd_rfd); #endif if (NULL != h->mhd_bank) { MHD_stop_daemon (h->mhd_bank); h->mhd_bank = NULL; } GNUNET_free (h); } /** * Create and queue a bank error message with the HTTP response * code @a response_code on connection @a connection. * * @param connection where to queue the reply * @param response_code http status code to use * @param ec taler error code to use * @param message human readable error message * @return MHD status code */ static int create_bank_error (struct MHD_Connection *connection, unsigned int response_code, enum TALER_ErrorCode ec, const char *message) { json_t *json; struct MHD_Response *resp; void *json_str; size_t json_len; int ret; json = json_pack ("{s:s, s:I}", "error", message, "ec", (json_int_t) ec); json_str = json_dumps (json, JSON_INDENT(2)); json_decref (json); if (NULL == json_str) { GNUNET_break (0); return MHD_NO; } json_len = strlen (json_str); resp = MHD_create_response_from_buffer (json_len, json_str, MHD_RESPMEM_MUST_FREE); if (NULL == resp) { GNUNET_break (0); free (json_str); return MHD_NO; } (void) MHD_add_response_header (resp, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json"); ret = MHD_queue_response (connection, response_code, resp); MHD_destroy_response (resp); return ret; } /** * Function called whenever MHD is done with a request. If the * request was a POST, we may have stored a `struct Buffer *` in the * @a con_cls that might still need to be cleaned up. Call the * respective function to free the memory. * * @param cls client-defined closure * @param connection connection handle * @param con_cls value as set by the last call to * the #MHD_AccessHandlerCallback * @param toe reason for request termination * @see #MHD_OPTION_NOTIFY_COMPLETED * @ingroup request */ static void handle_mhd_completion_callback (void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { /* struct TALER_FAKEBANK_Handle *h = cls; */ GNUNET_JSON_post_parser_cleanup (*con_cls); *con_cls = NULL; } /** * Handle incoming HTTP request for /admin/add/incoming. * * @param h the fakebank handle * @param connection the connection * @param upload_data request data * @param upload_data_size size of @a upload_data in bytes * @param con_cls closure for request (a `struct Buffer *`) * @return MHD result code */ static int handle_admin_add_incoming (struct TALER_FAKEBANK_Handle *h, struct MHD_Connection *connection, const char *upload_data, size_t *upload_data_size, void **con_cls) { enum GNUNET_JSON_PostResult pr; json_t *json; struct MHD_Response *resp; int ret; uint64_t row_id; pr = GNUNET_JSON_post_parser (REQUEST_BUFFER_MAX, con_cls, upload_data, upload_data_size, &json); switch (pr) { case GNUNET_JSON_PR_OUT_OF_MEMORY: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_CONTINUE: return MHD_YES; case GNUNET_JSON_PR_REQUEST_TOO_LARGE: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_JSON_INVALID: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_SUCCESS: break; } { const char *subject; uint64_t debit_account; uint64_t credit_account; const char *base_url; struct TALER_Amount amount; char *amount_s; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("subject", &subject), GNUNET_JSON_spec_uint64 ("debit_account", &debit_account), GNUNET_JSON_spec_uint64 ("credit_account", &credit_account), TALER_JSON_spec_amount ("amount", &amount), GNUNET_JSON_spec_string ("exchange_url", &base_url), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (json, spec, NULL, NULL)) { GNUNET_break (0); json_decref (json); return MHD_NO; } row_id = TALER_FAKEBANK_make_transfer (h, debit_account, credit_account, &amount, subject, base_url); amount_s = TALER_amount_to_string (&amount); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Receiving incoming wire transfer: %llu->%llu, subject: %s, amount: %s, from %s\n", (unsigned long long) debit_account, (unsigned long long) credit_account, subject, amount_s, base_url); GNUNET_free (amount_s); } json_decref (json); /* Finally build response object */ { void *json_str; size_t json_len; json = json_pack ("{s:I}", "row_id", (json_int_t) row_id); json_str = json_dumps (json, JSON_INDENT(2)); json_decref (json); if (NULL == json_str) { GNUNET_break (0); return MHD_NO; } json_len = strlen (json_str); resp = MHD_create_response_from_buffer (json_len, json_str, MHD_RESPMEM_MUST_FREE); if (NULL == resp) { GNUNET_break (0); free (json_str); return MHD_NO; } (void) MHD_add_response_header (resp, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json"); } ret = MHD_queue_response (connection, MHD_HTTP_OK, resp); MHD_destroy_response (resp); return ret; } /** * Handle incoming HTTP request for /reject. * * @param h the fakebank handle * @param connection the connection * @param upload_data request data * @param upload_data_size size of @a upload_data in bytes * @param con_cls closure for request (a `struct Buffer *`) * @return MHD result code */ static int handle_reject (struct TALER_FAKEBANK_Handle *h, struct MHD_Connection *connection, const char *upload_data, size_t *upload_data_size, void **con_cls) { enum GNUNET_JSON_PostResult pr; json_t *json; struct MHD_Response *resp; int ret; int found; pr = GNUNET_JSON_post_parser (REQUEST_BUFFER_MAX, con_cls, upload_data, upload_data_size, &json); switch (pr) { case GNUNET_JSON_PR_OUT_OF_MEMORY: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_CONTINUE: return MHD_YES; case GNUNET_JSON_PR_REQUEST_TOO_LARGE: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_JSON_INVALID: GNUNET_break (0); return MHD_NO; case GNUNET_JSON_PR_SUCCESS: break; } { uint64_t row_id; uint64_t credit_account; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_uint64 ("row_id", &row_id), GNUNET_JSON_spec_uint64 ("account_number", &credit_account), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (json, spec, NULL, NULL)) { GNUNET_break (0); json_decref (json); return MHD_NO; } found = TALER_FAKEBANK_reject_transfer (h, row_id, credit_account); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Rejected wire transfer #%llu (to %llu)\n", (unsigned long long) row_id, (unsigned long long) credit_account); } json_decref (json); if (GNUNET_OK != found) return create_bank_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_BANK_REJECT_TRANSACTION_NOT_FOUND, "transaction unknown"); /* finally build regular response */ resp = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response (connection, MHD_HTTP_NO_CONTENT, resp); MHD_destroy_response (resp); return ret; } /*********************************** * Serving "/history" starts here. * ***********************************/ /** * Decides whether the history builder will advance or not * to the next element. * * @param ha history args * @return GNUNET_YES/NO to advance/not-advance. */ static int handle_history_advance (const struct HistoryArgs *ha, struct Transaction *pos) { const struct HistoryRangeIds *hri = ha->range; return (NULL != pos) && (0 != hri->count); } /** * Iterates on the "next" element to be processed. To * be used when the current element does not get inserted in * the result. * * @param ha history arguments. * @param pos current element being processed. * @return the next element to be processed. */ static struct Transaction * handle_history_skip (const struct HistoryArgs *ha, const struct Transaction *pos) { const struct HistoryRangeIds *hri = ha->range; if (hri->count > 0) return pos->next; if (hri->count < 0) return pos->prev; } /** * Iterates on the "next" element to be processed. To * be used when the current element _gets_ inserted in the result. * * @param ha history arguments. * @param pos current element being processed. * @return the next element to be processed. */ static struct Transaction * handle_history_step (const struct HistoryArgs *ha, const struct Transaction *pos) { struct HistoryRangeIds *hri = ha->range; if (hri->count > 0) { hri->count--; return pos->next; } if (hri->count < 0) { hri->count++; return pos->prev; } } /** * Decides whether the history builder will advance or not * to the next element. * * @param ha history args * @return GNUNET_YES/NO to advance/not-advance. */ static int handle_history_range_advance (const struct HistoryArgs *ha) { const struct HistoryRangeDates *hrd = ha->range; } /** * Iterates towards the "next" element to be processed. To * be used when the current element does not get inserted in * the result. * * @param ha history arguments. * @param pos current element being processed. * @return the next element to be processed. */ static struct Transaction * handle_history_range_skip (const struct HistoryArgs *ha) { const struct HistoryRangeDates *hrd = ha->range; } /** * Iterates on the "next" element to be processed. To * be used when the current element _gets_ inserted in the result. * * @param ha history arguments. * @param pos current element being processed. * @return the next element to be processed. */ static struct Transaction * handle_history_range_step (const struct HistoryArgs *ha) { const struct HistoryRangeDates *hrd = ha->range; } /** * Actual history response builder. * * @param pos first (included) element in the result set. * @param ha history arguments. * @param caller_name which function is building the history. * @return MHD_YES / MHD_NO, after having enqueued the response * object into MHD. */ static int build_history_response (struct MHD_Connection *connection, struct Transaction *pos, struct HistoryArgs *ha, Skip skip, Step step, CheckAdvance advance) { struct HistoryElement *history_results_head = NULL; struct HistoryElement *history_results_tail = NULL; struct HistoryElement *history_element = NULL; json_t *history; json_t *jresponse; int ret; while (GNUNET_YES == advance (ha, pos)) { json_t *trans; char *subject; const char *sign; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Found transaction over %s from %llu to %llu\n", TALER_amount2s (&pos->amount), (unsigned long long) pos->debit_account, (unsigned long long) pos->credit_account); if ( (! ( ( (ha->account_number == pos->debit_account) && (0 != (ha->direction & TALER_BANK_DIRECTION_DEBIT)) ) || ( (ha->account_number == pos->credit_account) && (0 != (ha->direction & TALER_BANK_DIRECTION_CREDIT) ) ) ) ) || ( (0 == (ha->direction & TALER_BANK_DIRECTION_CANCEL)) && (GNUNET_YES == pos->rejected) ) ) { pos = skip (ha, pos); continue; } GNUNET_asprintf (&subject, "%s %s", pos->subject, pos->exchange_base_url); sign = (ha->account_number == pos->debit_account) ? (pos->rejected ? "cancel-" : "-") : (pos->rejected ? "cancel+" : "+"); trans = json_pack ("{s:I, s:o, s:o, s:s, s:I, s:s}", "row_id", (json_int_t) pos->row_id, "date", GNUNET_JSON_from_time_abs (pos->date), "amount", TALER_JSON_from_amount (&pos->amount), "sign", sign, "counterpart", (json_int_t) ( (ha->account_number == pos->debit_account) ? pos->credit_account : pos->debit_account), "wt_subject", subject); GNUNET_assert (NULL != trans); GNUNET_free (subject); history_element = GNUNET_new (struct HistoryElement); history_element->element = trans; /* XXX: the ordering feature is missing. */ GNUNET_CONTAINER_DLL_insert_tail (history_results_head, history_results_tail, history_element); pos = step (ha, pos); } history = json_array (); if (NULL != history_results_head) history_element = history_results_head; while (NULL != history_element) { json_array_append_new (history, history_element->element); history_element = history_element->next; if (NULL != history_element) GNUNET_free_non_null (history_element->prev); } GNUNET_free_non_null (history_results_tail); if (0 == json_array_size (history)) { struct MHD_Response *resp; json_decref (history); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning empty transaction history\n"); resp = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response (connection, MHD_HTTP_NO_CONTENT, resp); MHD_destroy_response (resp); return ret; } jresponse = json_pack ("{s:o}", "data", history); if (NULL == jresponse) { GNUNET_break (0); return MHD_NO; } /* Finally build response object */ { struct MHD_Response *resp; void *json_str; size_t json_len; json_str = json_dumps (jresponse, JSON_INDENT(2)); json_decref (jresponse); if (NULL == json_str) { GNUNET_break (0); return MHD_NO; } json_len = strlen (json_str); resp = MHD_create_response_from_buffer (json_len, json_str, MHD_RESPMEM_MUST_FREE); if (NULL == resp) { GNUNET_break (0); free (json_str); return MHD_NO; } (void) MHD_add_response_header (resp, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json"); ret = MHD_queue_response (connection, MHD_HTTP_OK, resp); MHD_destroy_response (resp); } return ret; } /** * Parse URL history arguments, of _both_ APIs: * /history and /history-range. * * @param connection MHD connection. * @param function_name name of the caller. * @param ha[out] will contain the parsed values. * @return GNUNET_OK only if the parsing succeedes. */ static int parse_history_common_args (struct MHD_Connection *connection, struct HistoryArgs *ha) { /** * @variable * Just check if given and == "basic", no need to keep around. */ const char *auth; /** * All those will go into the structure, after parsing. */ const char *direction; const char *cancelled; const char *ordering; const char *account_number; auth = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "auth"); direction = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "direction"); cancelled = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "cancelled"); ordering = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "ordering"); account_number = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "account_number"); /* Fail if one of the above failed. */ if ( (NULL == direction) || (NULL == cancelled) || ( (0 != strcasecmp (cancelled, "OMIT")) && (0 != strcasecmp (cancelled, "SHOW")) ) || ( (0 != strcasecmp (direction, "BOTH")) && (0 != strcasecmp (direction, "CREDIT")) && (0 != strcasecmp (direction, "DEBIT")) ) || (1 != sscanf (account_number, "%llu", &ha->account_number)) || ( (NULL == auth) || (0 != strcasecmp (auth, "basic")) ) ) { /* Invalid request, given that this is fakebank we impolitely * just kill the connection instead of returning a nice error. */ GNUNET_break (0); return GNUNET_NO; } if (0 == strcasecmp (direction, "CREDIT")) { ha->direction = TALER_BANK_DIRECTION_CREDIT; } else if (0 == strcasecmp (direction, "DEBIT")) { ha->direction = TALER_BANK_DIRECTION_DEBIT; } else if (0 == strcasecmp (direction, "BOTH")) { ha->direction = TALER_BANK_DIRECTION_BOTH; } /* Direction is invalid. */ else { GNUNET_break (0); return GNUNET_NO; } if (0 == strcasecmp (cancelled, "OMIT")) { /* nothing */ } else if (0 == strcasecmp (cancelled, "SHOW")) { ha->direction |= TALER_BANK_DIRECTION_CANCEL; } /* Cancel-showing policy is invalid. */ else { GNUNET_break (0); return GNUNET_NO; } if ((NULL != ordering) && 0 == strcmp ("ascending", ordering)) ha->ascending = GNUNET_YES; else ha->ascending = GNUNET_NO; return GNUNET_OK; } /** * Handle incoming HTTP request for /history * * @param h the fakebank handle * @param connection the connection * @param con_cls place to store state, not used * @return MHD result code */ static int handle_history_new (struct TALER_FAKEBANK_Handle *h, struct MHD_Connection *connection, void **con_cls) { struct HistoryArgs ha; struct HistoryRangeIds hri; const char *start; const char *delta; struct Transaction *pos; if (GNUNET_OK != parse_history_common_args (connection, &ha)) { GNUNET_break (0); return MHD_NO; } start = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "start"); delta = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "delta"); if ( ((NULL != start) && (1 != sscanf (start, "%llu", &hri.start))) || (NULL == delta) || (1 != sscanf (delta, "%lld", &hri.count)) ) { GNUNET_break (0); return MHD_NO; } if (NULL == start) pos = 0 > hri.count ? h->transactions_tail : h->transactions_head; else if (NULL != h->transactions_head) { for (pos = h->transactions_head; NULL != pos; pos = pos->next) if (pos->row_id == hri.start) break; if (NULL == pos) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Invalid range specified," " transaction %llu not known!\n", (unsigned long long) hri.start); return MHD_NO; } /* range is exclusive, skip the matching entry */ if (hri.count > 0) pos = pos->next; if (hri.count < 0) pos = pos->prev; } else { /* list is empty */ pos = NULL; } return build_history_response (connection, pos, &ha, &handle_history_skip, &handle_history_step, &handle_history_advance); } /** * Handle incoming HTTP request for /history-range. * * @param h the fakebank handle * @param connection the connection * @param con_cls place to store state, not used * @return MHD result code */ static int handle_history_range (struct TALER_FAKEBANK_Handle *h, struct MHD_Connection *connection, void **con_cls) { struct HistoryArgs ha; struct HistoryRangeDates hrd; const char *start; const char *end; long long unsigned int start_stamp; long long unsigned int end_stamp; if (GNUNET_OK != parse_history_common_args (connection, &ha)) { GNUNET_break (0); return MHD_NO; } start = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "start"); end = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "end"); if ( (NULL == start) || (1 != sscanf (start, "%llu", &start_stamp)) || (NULL == end) || (1 != sscanf (end, "%lld", &end_stamp)) ) { GNUNET_break (0); return GNUNET_NO; } hrd.start.abs_value_us = start_stamp * 1000LL * 1000LL; hrd.end.abs_value_us = end_stamp * 1000LL * 1000LL; } /** * Handle incoming HTTP request for /history * * @param h the fakebank handle * @param connection the connection * @param con_cls place to store state, not used * @return MHD result code */ static int handle_history (struct TALER_FAKEBANK_Handle *h, struct MHD_Connection *connection, void **con_cls) { const char *auth; const char *delta; const char *start; const char *dir; const char *acc; const char *cancelled; const char *ordering; unsigned long long account_number; unsigned long long start_number; long long count; enum TALER_BANK_Direction direction; struct Transaction *pos; json_t *history; json_t *jresponse; int ret; int ascending; struct HistoryElement *history_results_head = NULL; struct HistoryElement *history_results_tail = NULL; struct HistoryElement *history_element = NULL; auth = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "auth"); delta = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "delta"); dir = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "direction"); cancelled = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "cancelled"); start = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "start"); ordering = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "ordering"); acc = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "account_number"); if ( (NULL == auth) || (0 != strcasecmp (auth, "basic")) || (NULL == acc) || (NULL == delta) ) { /* Invalid request, given that this is fakebank we impolitely just kill the connection instead of returning a nice error. */ GNUNET_break (0); return MHD_NO; } start_number = 0; if ( (1 != sscanf (delta, "%lld", &count)) || (1 != sscanf (acc, "%llu", &account_number)) || ( (NULL != start) && (1 != sscanf (start, "%llu", &start_number)) ) || (NULL == dir) || (NULL == cancelled) || ( (0 != strcasecmp (cancelled, "OMIT")) && (0 != strcasecmp (cancelled, "SHOW")) ) || ( (0 != strcasecmp (dir, "BOTH")) && (0 != strcasecmp (dir, "CREDIT")) && (0 != strcasecmp (dir, "DEBIT")) ) ) { /* Invalid request, given that this is fakebank we impolitely * just kill the connection instead of returning a nice error. */ GNUNET_break (0); return MHD_NO; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client asked for up to %lld results of type %s for account %llu starting at %llu\n", count, dir, (unsigned long long) account_number, start_number); if (0 == strcasecmp (dir, "CREDIT")) { direction = TALER_BANK_DIRECTION_CREDIT; } else if (0 == strcasecmp (dir, "DEBIT")) { direction = TALER_BANK_DIRECTION_DEBIT; } else if (0 == strcasecmp (dir, "BOTH")) { direction = TALER_BANK_DIRECTION_BOTH; } else { GNUNET_assert (0); return MHD_NO; } if (0 == strcasecmp (cancelled, "OMIT")) { /* nothing */ } else if (0 == strcasecmp (cancelled, "SHOW")) { direction |= TALER_BANK_DIRECTION_CANCEL; } else { GNUNET_assert (0); return MHD_NO; } if (NULL == start) pos = 0 > count ? h->transactions_tail : h->transactions_head; else if (NULL != h->transactions_head) { for (pos = h->transactions_head; NULL != pos; pos = pos->next) if (pos->row_id == start_number) break; if (NULL == pos) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Invalid range specified, transaction %llu not known!\n", (unsigned long long) start_number); return MHD_NO; } /* range is exclusive, skip the matching entry */ if (count > 0) pos = pos->next; if (count < 0) pos = pos->prev; } else { /* list is empty */ pos = NULL; } history = json_array (); if ((NULL != ordering) && 0 == strcmp ("ascending", ordering)) ascending = GNUNET_YES; else ascending = GNUNET_NO; while ( (NULL != pos) && (0 != count) ) { json_t *trans; char *subject; const char *sign; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Found transaction over %s from %llu to %llu\n", TALER_amount2s (&pos->amount), (unsigned long long) pos->debit_account, (unsigned long long) pos->credit_account); if ( (! ( ( (account_number == pos->debit_account) && (0 != (direction & TALER_BANK_DIRECTION_DEBIT)) ) || ( (account_number == pos->credit_account) && (0 != (direction & TALER_BANK_DIRECTION_CREDIT) ) ) ) ) || ( (0 == (direction & TALER_BANK_DIRECTION_CANCEL)) && (GNUNET_YES == pos->rejected) ) ) { if (count > 0) pos = pos->next; if (count < 0) pos = pos->prev; continue; } GNUNET_asprintf (&subject, "%s %s", pos->subject, pos->exchange_base_url); sign = (account_number == pos->debit_account) ? (pos->rejected ? "cancel-" : "-") : (pos->rejected ? "cancel+" : "+"); trans = json_pack ("{s:I, s:o, s:o, s:s, s:I, s:s}", "row_id", (json_int_t) pos->row_id, "date", GNUNET_JSON_from_time_abs (pos->date), "amount", TALER_JSON_from_amount (&pos->amount), "sign", sign, "counterpart", (json_int_t) ( (account_number == pos->debit_account) ? pos->credit_account : pos->debit_account), "wt_subject", subject); GNUNET_assert (NULL != trans); GNUNET_free (subject); history_element = GNUNET_new (struct HistoryElement); history_element->element = trans; if (((0 < count) && (GNUNET_YES == ascending)) || ((0 > count) && (GNUNET_NO == ascending))) GNUNET_CONTAINER_DLL_insert_tail (history_results_head, history_results_tail, history_element); else GNUNET_CONTAINER_DLL_insert (history_results_head, history_results_tail, history_element); if (count > 0) { pos = pos->next; count--; } if (count < 0) { pos = pos->prev; count++; } } if (NULL != history_results_head) history_element = history_results_head; while (NULL != history_element) { json_array_append_new (history, history_element->element); history_element = history_element->next; if (NULL != history_element) GNUNET_free_non_null (history_element->prev); } GNUNET_free_non_null (history_results_tail); if (0 == json_array_size (history)) { struct MHD_Response *resp; json_decref (history); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning empty transaction history\n"); resp = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); ret = MHD_queue_response (connection, MHD_HTTP_NO_CONTENT, resp); MHD_destroy_response (resp); return ret; } jresponse = json_pack ("{s:o}", "data", history); if (NULL == jresponse) { GNUNET_break (0); return MHD_NO; } /* Finally build response object */ { struct MHD_Response *resp; void *json_str; size_t json_len; json_str = json_dumps (jresponse, JSON_INDENT(2)); json_decref (jresponse); if (NULL == json_str) { GNUNET_break (0); return MHD_NO; } json_len = strlen (json_str); resp = MHD_create_response_from_buffer (json_len, json_str, MHD_RESPMEM_MUST_FREE); if (NULL == resp) { GNUNET_break (0); free (json_str); return MHD_NO; } (void) MHD_add_response_header (resp, MHD_HTTP_HEADER_CONTENT_TYPE, "application/json"); ret = MHD_queue_response (connection, MHD_HTTP_OK, resp); MHD_destroy_response (resp); } return ret; } /*********************************** * End of /history implementation. * ***********************************/ /** * Handle incoming HTTP request. * * @param cls a `struct TALER_FAKEBANK_Handle` * @param connection the connection * @param url the requested url * @param method the method (POST, GET, ...) * @param version HTTP version (ignored) * @param upload_data request data * @param upload_data_size size of @a upload_data in bytes * @param con_cls closure for request (a `struct Buffer *`) * @return MHD result code */ static int handle_mhd_request (void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { struct TALER_FAKEBANK_Handle *h = cls; if ( (0 == strcasecmp (url, "/admin/add/incoming")) && (0 == strcasecmp (method, MHD_HTTP_METHOD_POST)) ) return handle_admin_add_incoming (h, connection, upload_data, upload_data_size, con_cls); if ( (0 == strcasecmp (url, "/reject")) && (0 == strcasecmp (method, MHD_HTTP_METHOD_POST)) ) return handle_reject (h, connection, upload_data, upload_data_size, con_cls); if ( (0 == strcasecmp (url, "/history")) && (0 == strcasecmp (method, MHD_HTTP_METHOD_GET)) ) return handle_history (h, connection, con_cls); /* Unexpected URL path, just close the connection. */ /* we're rather impolite here, but it's a testcase. */ TALER_LOG_ERROR ("Breaking URL: %s\n", url); GNUNET_break_op (0); return MHD_NO; } /** * Task run whenever HTTP server operations are pending. * * @param cls the `struct TALER_FAKEBANK_Handle` */ static void run_mhd (void *cls); #if EPOLL_SUPPORT /** * Schedule MHD. This function should be called initially when an * MHD is first getting its client socket, and will then automatically * always be called later whenever there is work to be done. * * @param h fakebank handle to schedule MHD for */ static void schedule_httpd (struct TALER_FAKEBANK_Handle *h) { int haveto; MHD_UNSIGNED_LONG_LONG timeout; struct GNUNET_TIME_Relative tv; haveto = MHD_get_timeout (h->mhd_bank, &timeout); if (MHD_YES == haveto) tv.rel_value_us = (uint64_t) timeout * 1000LL; else tv = GNUNET_TIME_UNIT_FOREVER_REL; if (NULL != h->mhd_task) GNUNET_SCHEDULER_cancel (h->mhd_task); h->mhd_task = GNUNET_SCHEDULER_add_read_net (tv, h->mhd_rfd, &run_mhd, h); } #else /** * Schedule MHD. This function should be called initially when an * MHD is first getting its client socket, and will then automatically * always be called later whenever there is work to be done. * * @param h fakebank handle to schedule MHD for */ static void schedule_httpd (struct TALER_FAKEBANK_Handle *h) { fd_set rs; fd_set ws; fd_set es; struct GNUNET_NETWORK_FDSet *wrs; struct GNUNET_NETWORK_FDSet *wws; int max; int haveto; MHD_UNSIGNED_LONG_LONG timeout; struct GNUNET_TIME_Relative tv; FD_ZERO (&rs); FD_ZERO (&ws); FD_ZERO (&es); max = -1; if (MHD_YES != MHD_get_fdset (h->mhd_bank, &rs, &ws, &es, &max)) { GNUNET_assert (0); return; } haveto = MHD_get_timeout (h->mhd_bank, &timeout); if (MHD_YES == haveto) tv.rel_value_us = (uint64_t) timeout * 1000LL; else tv = GNUNET_TIME_UNIT_FOREVER_REL; if (-1 != max) { wrs = GNUNET_NETWORK_fdset_create (); wws = GNUNET_NETWORK_fdset_create (); GNUNET_NETWORK_fdset_copy_native (wrs, &rs, max + 1); GNUNET_NETWORK_fdset_copy_native (wws, &ws, max + 1); } else { wrs = NULL; wws = NULL; } if (NULL != h->mhd_task) GNUNET_SCHEDULER_cancel (h->mhd_task); h->mhd_task = GNUNET_SCHEDULER_add_select (GNUNET_SCHEDULER_PRIORITY_DEFAULT, tv, wrs, wws, &run_mhd, h); if (NULL != wrs) GNUNET_NETWORK_fdset_destroy (wrs); if (NULL != wws) GNUNET_NETWORK_fdset_destroy (wws); } #endif /** * Task run whenever HTTP server operations are pending. * * @param cls the `struct TALER_FAKEBANK_Handle` */ static void run_mhd (void *cls) { struct TALER_FAKEBANK_Handle *h = cls; h->mhd_task = NULL; MHD_run (h->mhd_bank); schedule_httpd (h); } /** * Start the fake bank. * * @param port port to listen to * @return NULL on error */ struct TALER_FAKEBANK_Handle * TALER_FAKEBANK_start (uint16_t port) { struct TALER_FAKEBANK_Handle *h; h = GNUNET_new (struct TALER_FAKEBANK_Handle); h->mhd_bank = MHD_start_daemon (MHD_USE_DEBUG #if EPOLL_SUPPORT | MHD_USE_EPOLL #endif | MHD_USE_DUAL_STACK, port, NULL, NULL, &handle_mhd_request, h, MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, h, MHD_OPTION_LISTEN_BACKLOG_SIZE, (unsigned int) 1024, MHD_OPTION_END); if (NULL == h->mhd_bank) { GNUNET_free (h); return NULL; } #if EPOLL_SUPPORT h->mhd_fd = MHD_get_daemon_info (h->mhd_bank, MHD_DAEMON_INFO_EPOLL_FD)->epoll_fd; h->mhd_rfd = GNUNET_NETWORK_socket_box_native (h->mhd_fd); #endif schedule_httpd (h); return h; } /* end of fakebank.c */