diff options
Diffstat (limited to 'src/bank-lib')
-rw-r--r-- | src/bank-lib/Makefile.am | 45 | ||||
-rw-r--r-- | src/bank-lib/bank_api_admin.c | 240 | ||||
-rw-r--r-- | src/bank-lib/bank_api_context.c | 570 | ||||
-rw-r--r-- | src/bank-lib/bank_api_context.h | 181 | ||||
-rw-r--r-- | src/bank-lib/bank_api_json.c | 525 | ||||
-rw-r--r-- | src/bank-lib/bank_api_json.h | 352 | ||||
-rw-r--r-- | src/bank-lib/test_bank_api.c | 542 |
7 files changed, 2455 insertions, 0 deletions
diff --git a/src/bank-lib/Makefile.am b/src/bank-lib/Makefile.am new file mode 100644 index 000000000..2f44adada --- /dev/null +++ b/src/bank-lib/Makefile.am @@ -0,0 +1,45 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +lib_LTLIBRARIES = \ + libtalerbank.la + +libtalerbank_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined + +libtalerbank_la_SOURCES = \ + bank_api_context.c bank_api_context.h \ + bank_api_json.c bank_api_json.h \ + bank_api_admin.c + +libtalerbank_la_LIBADD = \ + -lgnunetutil \ + -ljansson \ + $(XLIB) + +if HAVE_LIBCURL +libtalerbank_la_LIBADD += -lcurl +else +if HAVE_LIBGNURL +libtalerbank_la_LIBADD += -lgnurl +endif +endif + +check_PROGRAMS = \ + test_bank_api + +TESTS = \ + $(check_PROGRAMS) + +test_bank_api_SOURCES = \ + test_bank_api.c +test_bank_api_LDADD = \ + libtalerbank.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetutil diff --git a/src/bank-lib/bank_api_admin.c b/src/bank-lib/bank_api_admin.c new file mode 100644 index 000000000..bfcf16a23 --- /dev/null +++ b/src/bank-lib/bank_api_admin.c @@ -0,0 +1,240 @@ +/* + This file is part of TALER + Copyright (C) 2015, 2016 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, If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file bank-lib/bank_api_admin.c + * @brief Implementation of the /admin/ requests of the bank's HTTP API + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include "taler_bank_service.h" +#include "bank_api_json.h" +#include "bank_api_context.h" +#include "taler_signatures.h" + + +/** + * @brief An admin/add/incoming Handle + */ +struct TALER_BANK_AdminAddIncomingHandle +{ + + /** + * The connection to bank this request handle will use + */ + struct TALER_BANK_Context *bank; + + /** + * The url for this request. + */ + char *url; + + /** + * JSON encoding of the request to POST. + */ + char *json_enc; + + /** + * Handle for the request. + */ + struct BAC_Job *job; + + /** + * HTTP headers for the request. + */ + struct curl_slist *headers; + + /** + * Function to call with the result. + */ + TALER_BANK_AdminAddIncomingResultCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Download buffer + */ + struct BAC_DownloadBuffer db; + +}; + + +/** + * Function called when we're done processing the + * HTTP /admin/add/incoming request. + * + * @param cls the `struct TALER_BANK_AdminAddIncomingHandle` + * @param eh the curl request handle + */ +static void +handle_admin_add_incoming_finished (void *cls, + CURL *eh) +{ + struct TALER_BANK_AdminAddIncomingHandle *aai = cls; + long response_code; + json_t *json; + + aai->job = NULL; + json = BAC_download_get_result (&aai->db, + eh, + &response_code); + switch (response_code) + { + case 0: + break; + case MHD_HTTP_OK: + break; + case MHD_HTTP_BAD_REQUEST: + /* This should never happen, either us or the bank is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + /* Access denied */ + break; + case MHD_HTTP_UNAUTHORIZED: + /* Nothing really to verify, bank says one of the signatures is + invalid; as we checked them, this should never happen, we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + response_code); + GNUNET_break (0); + response_code = 0; + break; + } + aai->cb (aai->cb_cls, + response_code); + json_decref (json); + TALER_BANK_admin_add_incoming_cancel (aai); +} + + +/** + * Notify the bank that we have received an incoming transaction + * which fills a reserve. Note that this API is an administrative + * API and thus not accessible to typical bank clients, but only + * to the operators of the bank. + * + * @param bank the bank handle; the bank must be ready to operate + * @param reserve_pub public key of the reserve + * @param amount amount that was deposited + * @param execution_date when did we receive the amount + * @param account_no account number (53 bits at most) + * @param res_cb the callback to call when the final result for this request is available + * @param res_cb_cls closure for the above callback + * @return NULL + * if the inputs are invalid (i.e. invalid amount). + * In this case, the callback is not called. + */ +struct TALER_BANK_AdminAddIncomingHandle * +TALER_BANK_admin_add_incoming (struct TALER_BANK_Context *bank, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *amount, + uint64_t account_no, + TALER_BANK_AdminAddIncomingResultCallback res_cb, + void *res_cb_cls) +{ + struct TALER_BANK_AdminAddIncomingHandle *aai; + json_t *admin_obj; + CURL *eh; + + admin_obj = json_pack ("{s:o, s:o," /* reserve_pub/amount */ + " s:I}", /* execution_Date/wire */ + "wtid", TALER_json_from_data (wtid, + sizeof (*wtid)), + "amount", TALER_json_from_amount (amount), + "account", (json_int_t) account_no); + aai = GNUNET_new (struct TALER_BANK_AdminAddIncomingHandle); + aai->bank = bank; + aai->cb = res_cb; + aai->cb_cls = res_cb_cls; + aai->url = BAC_path_to_url (bank, "/admin/add/incoming"); + + eh = curl_easy_init (); + GNUNET_assert (NULL != (aai->json_enc = + json_dumps (admin_obj, + JSON_COMPACT))); + json_decref (admin_obj); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + aai->url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + aai->json_enc)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDSIZE, + strlen (aai->json_enc))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_WRITEFUNCTION, + &BAC_download_cb)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_WRITEDATA, + &aai->db)); + aai->job = BAC_job_add (bank, + eh, + GNUNET_YES, + &handle_admin_add_incoming_finished, + aai); + return aai; +} + + +/** + * Cancel an add incoming. This function cannot be used on a request + * handle if a response is already served for it. + * + * @param aai the admin add incoming request handle + */ +void +TALER_BANK_admin_add_incoming_cancel (struct TALER_BANK_AdminAddIncomingHandle *aai) +{ + if (NULL != aai->job) + { + BAC_job_cancel (aai->job); + aai->job = NULL; + } + curl_slist_free_all (aai->headers); + GNUNET_free_non_null (aai->db.buf); + GNUNET_free (aai->url); + GNUNET_free (aai->json_enc); + GNUNET_free (aai); +} + + +/* end of bank_api_admin.c */ diff --git a/src/bank-lib/bank_api_context.c b/src/bank-lib/bank_api_context.c new file mode 100644 index 000000000..a47b4072a --- /dev/null +++ b/src/bank-lib/bank_api_context.c @@ -0,0 +1,570 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 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, If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file bank-lib/bank_api_context.c + * @brief Implementation of the context part of the bank's HTTP API + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include "taler_bank_service.h" +#include "bank_api_context.h" + + +/** + * Log error related to CURL operations. + * + * @param type log level + * @param function which function failed to run + * @param code what was the curl error code + */ +#define CURL_STRERROR(type, function, code) \ + GNUNET_log (type, \ + "Curl function `%s' has failed at `%s:%d' with error: %s\n", \ + function, __FILE__, __LINE__, curl_easy_strerror (code)); + +/** + * Print JSON parsing related error information + */ +#define JSON_WARN(error) \ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, \ + "JSON parsing failed at %s:%u: %s (%s)\n", \ + __FILE__, __LINE__, error.text, error.source) + + +/** + * Failsafe flag. Raised if our constructor fails to initialize + * the Curl library. + */ +static int TALER_BANK_curl_fail; + + +/** + * Jobs are CURL requests running within a `struct TALER_BANK_Context`. + */ +struct BAC_Job +{ + + /** + * We keep jobs in a DLL. + */ + struct BAC_Job *next; + + /** + * We keep jobs in a DLL. + */ + struct BAC_Job *prev; + + /** + * Easy handle of the job. + */ + CURL *easy_handle; + + /** + * Context this job runs in. + */ + struct TALER_BANK_Context *ctx; + + /** + * Function to call upon completion. + */ + BAC_JobCompletionCallback jcc; + + /** + * Closure for @e jcc. + */ + void *jcc_cls; + +}; + + +/** + * Context + */ +struct TALER_BANK_Context +{ + /** + * Curl multi handle + */ + CURLM *multi; + + /** + * Curl share handle + */ + CURLSH *share; + + /** + * We keep jobs in a DLL. + */ + struct BAC_Job *jobs_head; + + /** + * We keep jobs in a DLL. + */ + struct BAC_Job *jobs_tail; + + /** + * HTTP header "application/json", created once and used + * for all requests that need it. + */ + struct curl_slist *json_header; + + /** + * Base URL of the bank. + */ + char *url; + +}; + + +/** + * Initialise this library. This function should be called before using any of + * the following functions. + * + * @param url HTTP base URL for the bank + * @return library context + */ +struct TALER_BANK_Context * +TALER_BANK_init (const char *url) +{ + struct TALER_BANK_Context *ctx; + CURLM *multi; + CURLSH *share; + + if (TALER_BANK_curl_fail) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Curl was not initialised properly\n"); + return NULL; + } + if (NULL == (multi = curl_multi_init ())) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to create a Curl multi handle\n"); + return NULL; + } + if (NULL == (share = curl_share_init ())) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to create a Curl share handle\n"); + return NULL; + } + ctx = GNUNET_new (struct TALER_BANK_Context); + ctx->multi = multi; + ctx->share = share; + ctx->url = GNUNET_strdup (url); + GNUNET_assert (NULL != (ctx->json_header = + curl_slist_append (NULL, + "Content-Type: application/json"))); + return ctx; +} + + +/** + * Schedule a CURL request to be executed and call the given @a jcc + * upon its completion. Note that the context will make use of the + * CURLOPT_PRIVATE facility of the CURL @a eh. Applications can + * instead use #BAC_easy_to_closure to extract the @a jcc_cls argument + * from a valid @a eh afterwards. + * + * This function modifies the CURL handle to add the + * "Content-Type: application/json" header if @a add_json is set. + * + * @param ctx context to execute the job in + * @param eh curl easy handle for the request, will + * be executed AND cleaned up + * @param add_json add "application/json" content type header + * @param jcc callback to invoke upon completion + * @param jcc_cls closure for @a jcc + */ +struct BAC_Job * +BAC_job_add (struct TALER_BANK_Context *ctx, + CURL *eh, + int add_json, + BAC_JobCompletionCallback jcc, + void *jcc_cls) +{ + struct BAC_Job *job; + + if (GNUNET_YES == add_json) + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HTTPHEADER, + ctx->json_header)); + + job = GNUNET_new (struct BAC_Job); + job->easy_handle = eh; + job->ctx = ctx; + job->jcc = jcc; + job->jcc_cls = jcc_cls; + GNUNET_CONTAINER_DLL_insert (ctx->jobs_head, + ctx->jobs_tail, + job); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_PRIVATE, + job)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_SHARE, + ctx->share)); + GNUNET_assert (CURLM_OK == + curl_multi_add_handle (ctx->multi, + eh)); + return job; +} + + +/** + * Obtain the `jcc_cls` argument from an `eh` that was + * given to #BAC_job_add(). + * + * @param eh easy handle that was used + * @return the `jcc_cls` that was given to #BAC_job_add(). + */ +void * +BAC_easy_to_closure (CURL *eh) +{ + struct BAC_Job *job; + + GNUNET_assert (CURLE_OK == + curl_easy_getinfo (eh, + CURLINFO_PRIVATE, + (char **) &job)); + return job->jcc_cls; +} + + +/** + * Cancel a job. Must only be called before the job completion + * callback is called for the respective job. + * + * @param job job to cancel + */ +void +BAC_job_cancel (struct BAC_Job *job) +{ + struct TALER_BANK_Context *ctx = job->ctx; + + GNUNET_CONTAINER_DLL_remove (ctx->jobs_head, + ctx->jobs_tail, + job); + GNUNET_assert (CURLM_OK == + curl_multi_remove_handle (ctx->multi, + job->easy_handle)); + curl_easy_cleanup (job->easy_handle); + GNUNET_free (job); +} + + +/** + * Run the main event loop for the Taler interaction. + * + * @param ctx the library context + */ +void +TALER_BANK_perform (struct TALER_BANK_Context *ctx) +{ + CURLMsg *cmsg; + struct BAC_Job *job; + int n_running; + int n_completed; + + (void) curl_multi_perform (ctx->multi, + &n_running); + while (NULL != (cmsg = curl_multi_info_read (ctx->multi, + &n_completed))) + { + /* Only documented return value is CURLMSG_DONE */ + GNUNET_break (CURLMSG_DONE == cmsg->msg); + GNUNET_assert (CURLE_OK == + curl_easy_getinfo (cmsg->easy_handle, + CURLINFO_PRIVATE, + (char **) &job)); + GNUNET_assert (job->ctx == ctx); + job->jcc (job->jcc_cls, + cmsg->easy_handle); + BAC_job_cancel (job); + } +} + + +/** + * Obtain the information for a select() call to wait until + * #TALER_BANK_perform() is ready again. Note that calling + * any other TALER_BANK-API may also imply that the library + * is again ready for #TALER_BANK_perform(). + * + * Basically, a client should use this API to prepare for select(), + * then block on select(), then call #TALER_BANK_perform() and then + * start again until the work with the context is done. + * + * This function will NOT zero out the sets and assumes that @a max_fd + * and @a timeout are already set to minimal applicable values. It is + * safe to give this API FD-sets and @a max_fd and @a timeout that are + * already initialized to some other descriptors that need to go into + * the select() call. + * + * @param ctx context to get the event loop information for + * @param read_fd_set will be set for any pending read operations + * @param write_fd_set will be set for any pending write operations + * @param except_fd_set is here because curl_multi_fdset() has this argument + * @param max_fd set to the highest FD included in any set; + * if the existing sets have no FDs in it, the initial + * value should be "-1". (Note that `max_fd + 1` will need + * to be passed to select().) + * @param timeout set to the timeout in milliseconds (!); -1 means + * no timeout (NULL, blocking forever is OK), 0 means to + * proceed immediately with #TALER_BANK_perform(). + */ +void +TALER_BANK_get_select_info (struct TALER_BANK_Context *ctx, + fd_set *read_fd_set, + fd_set *write_fd_set, + fd_set *except_fd_set, + int *max_fd, + long *timeout) +{ + long to; + int m; + + m = -1; + GNUNET_assert (CURLM_OK == + curl_multi_fdset (ctx->multi, + read_fd_set, + write_fd_set, + except_fd_set, + &m)); + to = *timeout; + *max_fd = GNUNET_MAX (m, *max_fd); + GNUNET_assert (CURLM_OK == + curl_multi_timeout (ctx->multi, + &to)); + + /* Only if what we got back from curl is smaller than what we + already had (-1 == infinity!), then update timeout */ + if ( (to < *timeout) && + (-1 != to) ) + *timeout = to; + if ( (-1 == (*timeout)) && + (NULL != ctx->jobs_head) ) + *timeout = to; +} + + +/** + * Cleanup library initialisation resources. This function should be called + * after using this library to cleanup the resources occupied during library's + * initialisation. + * + * @param ctx the library context + */ +void +TALER_BANK_fini (struct TALER_BANK_Context *ctx) +{ + /* all jobs must have been cancelled at this time, assert this */ + GNUNET_assert (NULL == ctx->jobs_head); + curl_share_cleanup (ctx->share); + curl_multi_cleanup (ctx->multi); + curl_slist_free_all (ctx->json_header); + GNUNET_free (ctx->url); + GNUNET_free (ctx); +} + + +/** + * Obtain the URL to use for an API request. + * + * @param h the mint handle to query + * @param path Taler API path (i.e. "/reserve/withdraw") + * @return the full URI to use with cURL + */ +char * +BAC_path_to_url (struct TALER_BANK_Context *h, + const char *path) +{ + char *url; + + if ( ('/' == path[0]) && + (0 < strlen (h->url)) && + ('/' == h->url[strlen (h->url) - 1]) ) + path++; /* avoid generating URL with "//" from concat */ + GNUNET_asprintf (&url, + "%s%s", + h->url, + path); + return url; +} + + +/** + * Callback used when downloading the reply to an HTTP request. + * Just appends all of the data to the `buf` in the + * `struct BAC_DownloadBuffer` for further processing. The size of + * the download is limited to #GNUNET_MAX_MALLOC_CHECKED, if + * the download exceeds this size, we abort with an error. + * + * @param bufptr data downloaded via HTTP + * @param size size of an item in @a bufptr + * @param nitems number of items in @a bufptr + * @param cls the `struct KeysRequest` + * @return number of bytes processed from @a bufptr + */ +size_t +BAC_download_cb (char *bufptr, + size_t size, + size_t nitems, + void *cls) +{ + struct BAC_DownloadBuffer *db = cls; + size_t msize; + void *buf; + + if (0 == size * nitems) + { + /* Nothing (left) to do */ + return 0; + } + msize = size * nitems; + if ( (msize + db->buf_size) >= GNUNET_MAX_MALLOC_CHECKED) + { + db->eno = ENOMEM; + return 0; /* signals an error to curl */ + } + db->buf = GNUNET_realloc (db->buf, + db->buf_size + msize); + buf = db->buf + db->buf_size; + memcpy (buf, bufptr, msize); + db->buf_size += msize; + return msize; +} + + +/** + * Obtain information about the final result about the + * HTTP download. If the download was successful, parses + * the JSON in the @a db and returns it. Also returns + * the HTTP @a response_code. If the download failed, + * the return value is NULL. The response code is set + * in any case, on download errors to zero. + * + * Calling this function also cleans up @a db. + * + * @param db download buffer + * @param eh CURL handle (to get the response code) + * @param[out] response_code set to the HTTP response code + * (or zero if we aborted the download, i.e. + * because the response was too big, or if + * the JSON we received was malformed). + * @return NULL if downloading a JSON reply failed + */ +json_t * +BAC_download_get_result (struct BAC_DownloadBuffer *db, + CURL *eh, + long *response_code) +{ + json_t *json; + json_error_t error; + char *ct; + + if ( (CURLE_OK != + curl_easy_getinfo (eh, + CURLINFO_CONTENT_TYPE, + &ct)) || + (NULL == ct) || + (0 != strcasecmp (ct, + "application/json")) ) + { + /* No content type or explicitly not JSON, refuse to parse + (but keep response code) */ + if (CURLE_OK != + curl_easy_getinfo (eh, + CURLINFO_RESPONSE_CODE, + response_code)) + { + /* unexpected error... */ + GNUNET_break (0); + *response_code = 0; + } + return NULL; + } + + json = NULL; + if (0 == db->eno) + { + json = json_loadb (db->buf, + db->buf_size, + JSON_REJECT_DUPLICATES | JSON_DISABLE_EOF_CHECK, + &error); + if (NULL == json) + { + JSON_WARN (error); + *response_code = 0; + } + } + GNUNET_free_non_null (db->buf); + db->buf = NULL; + db->buf_size = 0; + if (NULL != json) + { + if (CURLE_OK != + curl_easy_getinfo (eh, + CURLINFO_RESPONSE_CODE, + response_code)) + { + /* unexpected error... */ + GNUNET_break (0); + *response_code = 0; + } + } + return json; +} + + +/** + * Initial global setup logic, specifically runs the Curl setup. + */ +__attribute__ ((constructor)) +void +TALER_BANK_constructor__ (void) +{ + CURLcode ret; + + if (CURLE_OK != (ret = curl_global_init (CURL_GLOBAL_DEFAULT))) + { + CURL_STRERROR (GNUNET_ERROR_TYPE_ERROR, + "curl_global_init", + ret); + TALER_BANK_curl_fail = 1; + } +} + + +/** + * Cleans up after us, specifically runs the Curl cleanup. + */ +__attribute__ ((destructor)) +void +TALER_BANK_destructor__ (void) +{ + if (TALER_BANK_curl_fail) + return; + curl_global_cleanup (); +} + +/* end of bank_api_context.c */ diff --git a/src/bank-lib/bank_api_context.h b/src/bank-lib/bank_api_context.h new file mode 100644 index 000000000..552cbe440 --- /dev/null +++ b/src/bank-lib/bank_api_context.h @@ -0,0 +1,181 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 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, If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file bank-lib/bank_api_context.h + * @brief Internal interface to the context part of the bank's HTTP API + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include <gnunet/gnunet_util_lib.h> +#include "taler_bank_service.h" +#include "taler_signatures.h" + + +/** + * Entry in the context's job queue. + */ +struct BAC_Job; + +/** + * Function to call upon completion of a job. + * + * @param cls closure + * @param eh original easy handle (for inspection) + */ +typedef void +(*BAC_JobCompletionCallback)(void *cls, + CURL *eh); + + +/** + * Schedule a CURL request to be executed and call the given @a jcc + * upon its completion. Note that the context will make use of the + * CURLOPT_PRIVATE facility of the CURL @a eh. Applications can + * instead use #BAC_easy_to_closure to extract the @a jcc_cls argument + * from a valid @a eh afterwards. + * + * This function modifies the CURL handle to add the + * "Content-Type: application/json" header if @a add_json is set. + * + * @param ctx context to execute the job in + * @param eh curl easy handle for the request, will + * be executed AND cleaned up + * @param add_json add "application/json" content type header + * @param jcc callback to invoke upon completion + * @param jcc_cls closure for @a jcc + */ +struct BAC_Job * +BAC_job_add (struct TALER_BANK_Context *ctx, + CURL *eh, + int add_json, + BAC_JobCompletionCallback jcc, + void *jcc_cls); + + +/** + * Obtain the `jcc_cls` argument from an `eh` that was + * given to #BAC_job_add(). + * + * @param eh easy handle that was used + * @return the `jcc_cls` that was given to #BAC_job_add(). + */ +void * +BAC_easy_to_closure (CURL *eh); + + +/** + * Cancel a job. Must only be called before the job completion + * callback is called for the respective job. + * + * @param job job to cancel + */ +void +BAC_job_cancel (struct BAC_Job *job); + + +/** + * @brief Buffer data structure we use to buffer the HTTP download + * before giving it to the JSON parser. + */ +struct BAC_DownloadBuffer +{ + + /** + * Download buffer + */ + void *buf; + + /** + * The size of the download buffer + */ + size_t buf_size; + + /** + * Error code (based on libc errno) if we failed to download + * (i.e. response too large). + */ + int eno; + +}; + + +/** + * Callback used when downloading the reply to an HTTP request. + * Just appends all of the data to the `buf` in the + * `struct BAC_DownloadBuffer` for further processing. The size of + * the download is limited to #GNUNET_MAX_MALLOC_CHECKED, if + * the download exceeds this size, we abort with an error. + * + * Should be used by the various routines as the + * CURLOPT_WRITEFUNCTION. A `struct BAC_DownloadBuffer` needs to be + * passed to the CURLOPT_WRITEDATA. + * + * Afterwards, `eno` needs to be checked to ensure that the download + * completed correctly. + * + * @param bufptr data downloaded via HTTP + * @param size size of an item in @a bufptr + * @param nitems number of items in @a bufptr + * @param cls the `struct KeysRequest` + * @return number of bytes processed from @a bufptr + */ +size_t +BAC_download_cb (char *bufptr, + size_t size, + size_t nitems, + void *cls); + + +/** + * Obtain information about the final result about the + * HTTP download. If the download was successful, parses + * the JSON in the @a db and returns it. Also returns + * the HTTP @a response_code. If the download failed, + * the return value is NULL. The response code is set + * in any case, on download errors to zero. + * + * Calling this function also cleans up @a db. + * + * @param db download buffer + * @param eh CURL handle (to get the response code) + * @param[out] response_code set to the HTTP response code + * (or zero if we aborted the download, i.e. + * because the response was too big, or if + * the JSON we received was malformed). + * @return NULL if downloading a JSON reply failed + */ +json_t * +BAC_download_get_result (struct BAC_DownloadBuffer *db, + CURL *eh, + long *response_code); + + +/** + * Obtain the URL to use for an API request. + * + * @param h the bank handle to query + * @param path Taler API path (i.e. "/reserve/withdraw") + * @return the full URI to use with cURL + */ +char * +BAC_path_to_url (struct TALER_BANK_Context *h, + const char *path); + + +/* end of bank_api_context.h */ diff --git a/src/bank-lib/bank_api_json.c b/src/bank-lib/bank_api_json.c new file mode 100644 index 000000000..2a09e5272 --- /dev/null +++ b/src/bank-lib/bank_api_json.c @@ -0,0 +1,525 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file bank-lib/bank_api_json.c + * @brief functions to parse incoming requests (JSON snippets) + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include "bank_api_json.h" + +/** + * Navigate and parse data in a JSON tree. + * + * @param root the JSON node to start the navigation at. + * @param spec parse specification array + * @return offset in @a spec where parsing failed, -1 on success (!) + */ +static int +parse_json (json_t *root, + struct BAJ_Specification *spec) +{ + int i; + json_t *pos; /* what's our current position? */ + + pos = root; + for (i=0;BAJ_CMD_END != spec[i].cmd;i++) + { + pos = json_object_get (root, + spec[i].field); + if (NULL == pos) + { + GNUNET_break_op (0); + return i; + } + switch (spec[i].cmd) + { + case BAJ_CMD_END: + GNUNET_assert (0); + return i; + case BAJ_CMD_AMOUNT: + if (GNUNET_OK != + TALER_json_to_amount (pos, + spec[i].details.amount)) + { + GNUNET_break_op (0); + return i; + } + break; + case BAJ_CMD_TIME_ABSOLUTE: + if (GNUNET_OK != + TALER_json_to_abs (pos, + spec[i].details.abs_time)) + { + GNUNET_break_op (0); + return i; + } + break; + + case BAJ_CMD_STRING: + { + const char *str; + + str = json_string_value (pos); + if (NULL == str) + { + GNUNET_break_op (0); + return i; + } + *spec[i].details.strptr = str; + } + break; + + case BAJ_CMD_BINARY_FIXED: + { + const char *str; + int res; + + str = json_string_value (pos); + if (NULL == str) + { + GNUNET_break_op (0); + return i; + } + res = GNUNET_STRINGS_string_to_data (str, strlen (str), + spec[i].details.fixed_data.dest, + spec[i].details.fixed_data.dest_size); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return i; + } + } + break; + + case BAJ_CMD_BINARY_VARIABLE: + { + const char *str; + size_t size; + void *data; + int res; + + str = json_string_value (pos); + if (NULL == str) + { + GNUNET_break_op (0); + return i; + } + size = (strlen (str) * 5) / 8; + if (size >= 1024) + { + GNUNET_break_op (0); + return i; + } + data = GNUNET_malloc (size); + res = GNUNET_STRINGS_string_to_data (str, + strlen (str), + data, + size); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + GNUNET_free (data); + return i; + } + *spec[i].details.variable_data.dest_p = data; + *spec[i].details.variable_data.dest_size_p = size; + } + break; + + case BAJ_CMD_RSA_PUBLIC_KEY: + { + size_t size; + const char *str; + int res; + void *buf; + + str = json_string_value (pos); + if (NULL == str) + { + GNUNET_break_op (0); + return i; + } + size = (strlen (str) * 5) / 8; + buf = GNUNET_malloc (size); + res = GNUNET_STRINGS_string_to_data (str, + strlen (str), + buf, + size); + if (GNUNET_OK != res) + { + GNUNET_free (buf); + GNUNET_break_op (0); + return i; + } + *spec[i].details.rsa_public_key + = GNUNET_CRYPTO_rsa_public_key_decode (buf, + size); + GNUNET_free (buf); + if (NULL == spec[i].details.rsa_public_key) + { + GNUNET_break_op (0); + return i; + } + } + break; + + case BAJ_CMD_RSA_SIGNATURE: + { + size_t size; + const char *str; + int res; + void *buf; + + str = json_string_value (pos); + if (NULL == str) + { + GNUNET_break_op (0); + return i; + } + size = (strlen (str) * 5) / 8; + buf = GNUNET_malloc (size); + res = GNUNET_STRINGS_string_to_data (str, + strlen (str), + buf, + size); + if (GNUNET_OK != res) + { + GNUNET_free (buf); + GNUNET_break_op (0); + return i; + } + *spec[i].details.rsa_signature + = GNUNET_CRYPTO_rsa_signature_decode (buf, + size); + GNUNET_free (buf); + if (NULL == spec[i].details.rsa_signature) + return i; + } + break; + + case BAJ_CMD_UINT16: + { + json_int_t val; + + if (! json_is_integer (pos)) + { + GNUNET_break_op (0); + return i; + } + val = json_integer_value (pos); + if ( (0 > val) || (val > UINT16_MAX) ) + { + GNUNET_break_op (0); + return i; + } + *spec[i].details.u16 = (uint16_t) val; + } + break; + + case BAJ_CMD_UINT64: + { + json_int_t val; + + if (! json_is_integer (pos)) + { + GNUNET_break_op (0); + return i; + } + val = json_integer_value (pos); + *spec[i].details.u64 = (uint64_t) val; + } + break; + + case BAJ_CMD_JSON_OBJECT: + { + if (! (json_is_object (pos) || json_is_array (pos)) ) + { + GNUNET_break_op (0); + return i; + } + json_incref (pos); + *spec[i].details.obj = pos; + } + break; + + default: + GNUNET_break (0); + return i; + } + } + return -1; /* all OK! */ +} + + +/** + * Free all elements allocated during a + * #BAJ_parse_json() operation. + * + * @param spec specification of the parse operation + * @param end number of elements in @a spec to process + */ +static void +parse_free (struct BAJ_Specification *spec, + int end) +{ + int i; + + for (i=0;i<end;i++) + { + switch (spec[i].cmd) + { + case BAJ_CMD_END: + GNUNET_assert (0); + return; + case BAJ_CMD_AMOUNT: + break; + case BAJ_CMD_TIME_ABSOLUTE: + break; + case BAJ_CMD_BINARY_FIXED: + break; + case BAJ_CMD_STRING: + break; + case BAJ_CMD_BINARY_VARIABLE: + GNUNET_free (*spec[i].details.variable_data.dest_p); + *spec[i].details.variable_data.dest_p = NULL; + *spec[i].details.variable_data.dest_size_p = 0; + break; + case BAJ_CMD_RSA_PUBLIC_KEY: + GNUNET_CRYPTO_rsa_public_key_free (*spec[i].details.rsa_public_key); + *spec[i].details.rsa_public_key = NULL; + break; + case BAJ_CMD_RSA_SIGNATURE: + GNUNET_CRYPTO_rsa_signature_free (*spec[i].details.rsa_signature); + *spec[i].details.rsa_signature = NULL; + break; + case BAJ_CMD_JSON_OBJECT: + json_decref (*spec[i].details.obj); + *spec[i].details.obj = NULL; + break; + default: + GNUNET_break (0); + break; + } + } +} + + +/** + * Navigate and parse data in a JSON tree. + * + * @param root the JSON node to start the navigation at. + * @param spec parse specification array + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +int +BAJ_parse_json (const json_t *root, + struct BAJ_Specification *spec) +{ + int ret; + + ret = parse_json ((json_t *) root, + spec); + if (-1 == ret) + return GNUNET_OK; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "JSON field `%s` (%d) had unexpected value\n", + spec[ret].field, + ret); + parse_free (spec, ret); + return GNUNET_SYSERR; +} + + +/** + * Free all elements allocated during a + * #BAJ_parse_json() operation. + * + * @param spec specification of the parse operation + */ +void +BAJ_parse_free (struct BAJ_Specification *spec) +{ + int i; + + for (i=0;BAJ_CMD_END != spec[i].cmd;i++) ; + parse_free (spec, i); +} + + +/** + * The expected field stores a string. + * + * @param name name of the JSON field + * @param strptr where to store a pointer to the field + */ +struct BAJ_Specification +BAJ_spec_string (const char *name, + const char **strptr) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_STRING, + .field = name, + .details.strptr = strptr + }; + return ret; +} + + +/** + * Specification for parsing an absolute time value. + * + * @param name name of the JSON field + * @param at where to store the absolute time found under @a name + */ +struct BAJ_Specification +BAJ_spec_absolute_time (const char *name, + struct GNUNET_TIME_Absolute *at) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_TIME_ABSOLUTE, + .field = name, + .details.abs_time = at + }; + return ret; +} + + +/** + * Specification for parsing an amount value. + * + * @param name name of the JSON field + * @param amount where to store the amount found under @a name + */ +struct BAJ_Specification +BAJ_spec_amount (const char *name, + struct TALER_Amount *amount) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_AMOUNT, + .field = name, + .details.amount = amount + }; + return ret; +} + + +/** + * 16-bit integer. + * + * @param name name of the JSON field + * @param[out] u16 where to store the integer found under @a name + */ +struct BAJ_Specification +BAJ_spec_uint16 (const char *name, + uint16_t *u16) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_UINT16, + .field = name, + .details.u16 = u16 + }; + return ret; +} + + +/** + * 64-bit integer. + * + * @param name name of the JSON field + * @param[out] u64 where to store the integer found under @a name + */ +struct BAJ_Specification +BAJ_spec_uint64 (const char *name, + uint64_t *u64) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_UINT64, + .field = name, + .details.u64 = u64 + }; + return ret; +} + + +/** + * JSON object. + * + * @param name name of the JSON field + * @param[out] jsonp where to store the JSON found under @a name + */ +struct BAJ_Specification +BAJ_spec_json (const char *name, + json_t **jsonp) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_JSON_OBJECT, + .field = name, + .details.obj = jsonp + }; + return ret; +} + + +/** + * Specification for parsing an RSA public key. + * + * @param name name of the JSON field + * @param pk where to store the RSA key found under @a name + */ +struct BAJ_Specification +BAJ_spec_rsa_public_key (const char *name, + struct GNUNET_CRYPTO_rsa_PublicKey **pk) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_RSA_PUBLIC_KEY, + .field = name, + .details.rsa_public_key = pk + }; + return ret; +} + + +/** + * Specification for parsing an RSA signature. + * + * @param name name of the JSON field + * @param sig where to store the RSA signature found under @a name + */ +struct BAJ_Specification +BAJ_spec_rsa_signature (const char *name, + struct GNUNET_CRYPTO_rsa_Signature **sig) +{ + struct BAJ_Specification ret = + { + .cmd = BAJ_CMD_RSA_SIGNATURE, + .field = name, + .details.rsa_signature = sig + }; + return ret; +} + + +/* end of bank_api_json.c */ diff --git a/src/bank-lib/bank_api_json.h b/src/bank-lib/bank_api_json.h new file mode 100644 index 000000000..2ecaf8ef1 --- /dev/null +++ b/src/bank-lib/bank_api_json.h @@ -0,0 +1,352 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file mint-lib/mint_api_json.h + * @brief functions to parse incoming requests (JSON snippets) + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler_util.h" +#include <jansson.h> + + +/** + * Enumeration with the various commands for the + * #BAJ_parse_json interpreter. + */ +enum BAJ_Command +{ + + /** + * End of command list. + */ + BAJ_CMD_END, + + /** + * Parse amount at current position. + */ + BAJ_CMD_AMOUNT, + + /** + * Parse absolute time at current position. + */ + BAJ_CMD_TIME_ABSOLUTE, + + /** + * Parse fixed binary value at current position. + */ + BAJ_CMD_BINARY_FIXED, + + /** + * Parse variable-size binary value at current position. + */ + BAJ_CMD_BINARY_VARIABLE, + + /** + * Parse RSA public key at current position. + */ + BAJ_CMD_RSA_PUBLIC_KEY, + + /** + * Parse RSA signature at current position. + */ + BAJ_CMD_RSA_SIGNATURE, + + /** + * Parse `const char *` JSON string at current position. + */ + BAJ_CMD_STRING, + + /** + * Parse `uint16_t` integer at the current position. + */ + BAJ_CMD_UINT16, + + /** + * Parse `uint64_t` integer at the current position. + */ + BAJ_CMD_UINT64, + + /** + * Parse JSON object at the current position. + */ + BAJ_CMD_JSON_OBJECT, + + /** + * Parse ??? at current position. + */ + BAJ_CMD_C + +}; + + +/** + * @brief Entry in parser specification for #BAJ_parse_json. + */ +struct BAJ_Specification +{ + + /** + * Command to execute. + */ + enum BAJ_Command cmd; + + /** + * Name of the field to access. + */ + const char *field; + + /** + * Further details for the command. + */ + union { + + /** + * Where to store amount for #BAJ_CMD_AMOUNT. + */ + struct TALER_Amount *amount; + + /** + * Where to store time, for #BAJ_CMD_TIME_ABSOLUTE. + */ + struct GNUNET_TIME_Absolute *abs_time; + + /** + * Where to write binary data, for #BAJ_CMD_BINARY_FIXED. + */ + struct { + /** + * Where to write the data. + */ + void *dest; + + /** + * How many bytes to write to @e dest. + */ + size_t dest_size; + + } fixed_data; + + /** + * Where to write binary data, for #BAJ_CMD_BINARY_VARIABLE. + */ + struct { + /** + * Where to store the pointer with the data (is allocated). + */ + void **dest_p; + + /** + * Where to store the number of bytes allocated at `*dest`. + */ + size_t *dest_size_p; + + } variable_data; + + /** + * Where to store the RSA public key for #BAJ_CMD_RSA_PUBLIC_KEY + */ + struct GNUNET_CRYPTO_rsa_PublicKey **rsa_public_key; + + /** + * Where to store the RSA signature for #BAJ_CMD_RSA_SIGNATURE + */ + struct GNUNET_CRYPTO_rsa_Signature **rsa_signature; + + /** + * Details for #BAJ_CMD_EDDSA_SIGNATURE + */ + struct { + + /** + * Where to store the purpose. + */ + struct GNUNET_CRYPTO_EccSignaturePurpose **purpose_p; + + /** + * Key to verify the signature against. + */ + const struct GNUNET_CRYPTO_EddsaPublicKey *pub_key; + + } eddsa_signature; + + /** + * Where to store a pointer to the string. + */ + const char **strptr; + + /** + * Where to store 16-bit integer. + */ + uint16_t *u16; + + /** + * Where to store 64-bit integer. + */ + uint64_t *u64; + + /** + * Where to store a JSON object. + */ + json_t **obj; + + } details; + +}; + + +/** + * Navigate and parse data in a JSON tree. + * + * @param root the JSON node to start the navigation at. + * @param spec parse specification array + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +int +BAJ_parse_json (const json_t *root, + struct BAJ_Specification *spec); + + +/** + * Free all elements allocated during a + * #BAJ_parse_json() operation. + * + * @param spec specification of the parse operation + */ +void +BAJ_parse_free (struct BAJ_Specification *spec); + + +/** + * End of a parser specification. + */ +#define BAJ_spec_end { .cmd = BAJ_CMD_END } + +/** + * Fixed size object (in network byte order, encoded using Crockford + * Base32hex encoding). + * + * @param name name of the JSON field + * @param obj pointer where to write the data (type of `*obj` will determine size) + */ +#define BAJ_spec_fixed_auto(name,obj) { .cmd = BAJ_CMD_BINARY_FIXED, .field = name, .details.fixed_data.dest = obj, .details.fixed_data.dest_size = sizeof (*obj) } + + +/** + * Variable size object (in network byte order, encoded using Crockford + * Base32hex encoding). + * + * @param name name of the JSON field + * @param obj pointer where to write the data (a `void **`) + * @param size where to store the number of bytes allocated for @a obj (of type `size_t *` + */ +#define BAJ_spec_varsize(name,obj,size) { .cmd = BAJ_CMD_BINARY_VARIABLE, .field = name, .details.variable_data.dest_p = obj, .details.variable_data.dest_size_p = size } + + +/** + * The expected field stores a string. + * + * @param name name of the JSON field + * @param strptr where to store a pointer to the field + */ +struct BAJ_Specification +BAJ_spec_string (const char *name, + const char **strptr); + + +/** + * Absolute time. + * + * @param name name of the JSON field + * @param[out] at where to store the absolute time found under @a name + */ +struct BAJ_Specification +BAJ_spec_absolute_time (const char *name, + struct GNUNET_TIME_Absolute *at); + + +/** + * 16-bit integer. + * + * @param name name of the JSON field + * @param[out] u16 where to store the integer found under @a name + */ +struct BAJ_Specification +BAJ_spec_uint16 (const char *name, + uint16_t *u16); + + +/** + * 64-bit integer. + * + * @param name name of the JSON field + * @param[out] u64 where to store the integer found under @a name + */ +struct BAJ_Specification +BAJ_spec_uint64 (const char *name, + uint64_t *u64); + + +/** + * JSON object. + * + * @param name name of the JSON field + * @param[out] jsonp where to store the JSON found under @a name + */ +struct BAJ_Specification +BAJ_spec_json (const char *name, + json_t **jsonp); + + +/** + * Specification for parsing an amount value. + * + * @param name name of the JSON field + * @param amount where to store the amount under @a name + */ +struct BAJ_Specification +BAJ_spec_amount (const char *name, + struct TALER_Amount *amount); + + +/** + * Specification for parsing an RSA public key. + * + * @param name name of the JSON field + * @param pk where to store the RSA key found under @a name + */ +struct BAJ_Specification +BAJ_spec_rsa_public_key (const char *name, + struct GNUNET_CRYPTO_rsa_PublicKey **pk); + + +/** + * Specification for parsing an RSA signature. + * + * @param name name of the JSON field + * @param sig where to store the RSA signature found under @a name + */ +struct BAJ_Specification +BAJ_spec_rsa_signature (const char *name, + struct GNUNET_CRYPTO_rsa_Signature **sig); + + + + +/* end of mint_api_json.h */ diff --git a/src/bank-lib/test_bank_api.c b/src/bank-lib/test_bank_api.c new file mode 100644 index 000000000..b14f523ba --- /dev/null +++ b/src/bank-lib/test_bank_api.c @@ -0,0 +1,542 @@ +/* + This file is part of TALER + Copyright (C) 2016 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, If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file bank/test_bank_api.c + * @brief testcase to test bank's HTTP API interface + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_util.h" +#include "taler_signatures.h" +#include "taler_bank_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> + + +/** + * Main execution context for the main loop. + */ +static struct TALER_BANK_Context *ctx; + +/** + * Task run on shutdown. + */ +static struct GNUNET_SCHEDULER_Task *shutdown_task; + +/** + * Task that runs the main event loop. + */ +static struct GNUNET_SCHEDULER_Task *ctx_task; + +/** + * 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, + + /** + * Add funds to a reserve by (faking) incoming wire transfer. + */ + OC_ADMIN_ADD_INCOMING + +}; + + +/** + * Details for a bank 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 + { + + /** + * String describing the amount to add to the reserve. + */ + const char *amount; + + /** + * Account number. + */ + uint64_t account_no; + + /** + * Wire transfer identifier to use. Initialized to + * a random value. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Set to the API's handle during the operation. + */ + struct TALER_BANK_AdminAddIncomingHandle *aih; + + } admin_add_incoming; + + } details; + +}; + + +/** + * State of the interpreter loop. + */ +struct InterpreterState +{ + /** + * Keys from the bank. + */ + const struct TALER_BANK_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; + +}; + + +/** + * Task that runs the context's event loop with the GNUnet scheduler. + * + * @param cls unused + * @param tc scheduler context (unused) + */ +static void +context_task (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc); + + +/** + * Run the context task, the working set has changed. + */ +static void +trigger_context_task () +{ + GNUNET_SCHEDULER_cancel (ctx_task); + ctx_task = GNUNET_SCHEDULER_add_now (&context_task, + NULL); +} + + +/** + * The testcase failed, return with an error code. + * + * @param is interpreter state to clean up + */ +static void +fail (struct InterpreterState *is) +{ + result = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); +} + + +#if 0 +/** + * 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; +} +#endif + + +/** + * Run the main interpreter loop that performs bank operations. + * + * @param cls contains the `struct InterpreterState` + * @param tc scheduler context + */ +static void +interpreter_run (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc); + + +/** + * 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 bank's reply is bogus (fails to follow the protocol) + */ +static void +add_incoming_cb (void *cls, + unsigned int http_status) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + + cmd->details.admin_add_incoming.aih = NULL; + if (cmd->expected_response_code != http_status) + { + GNUNET_break (0); + fail (is); + return; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * Run the main interpreter loop that performs bank operations. + * + * @param cls contains the `struct InterpreterState` + * @param tc scheduler context + */ +static void +interpreter_run (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + struct TALER_Amount amount; + + is->task = NULL; + if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) + { + fprintf (stderr, + "Test aborted by shutdown request\n"); + fail (is); + return; + } + switch (cmd->oc) + { + case OC_END: + result = GNUNET_OK; + GNUNET_SCHEDULER_shutdown (); + return; + case OC_ADMIN_ADD_INCOMING: + + 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; + } + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &cmd->details.admin_add_incoming.wtid, + sizeof (cmd->details.admin_add_incoming.wtid)); + cmd->details.admin_add_incoming.aih + = TALER_BANK_admin_add_incoming (ctx, + &cmd->details.admin_add_incoming.wtid, + &amount, + cmd->details.admin_add_incoming.account_no, + &add_incoming_cb, + is); + if (NULL == cmd->details.admin_add_incoming.aih) + { + GNUNET_break (0); + fail (is); + return; + } + trigger_context_task (); + return; + 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 terminates (good or bad). + * Cleans up our state. + * + * @param cls the interpreter state. + * @param tc unused + */ +static void +do_shutdown (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc) +{ + struct InterpreterState *is = cls; + struct Command *cmd; + unsigned int i; + + shutdown_task = NULL; + for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) + { + switch (cmd->oc) + { + case OC_END: + GNUNET_assert (0); + 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_BANK_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih); + cmd->details.admin_add_incoming.aih = NULL; + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "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); + if (NULL != ctx_task) + { + GNUNET_SCHEDULER_cancel (ctx_task); + ctx_task = NULL; + } + if (NULL != ctx) + { + TALER_BANK_fini (ctx); + ctx = NULL; + } +} + + +/** + * Task that runs the context's event loop with the GNUnet scheduler. + * + * @param cls unused + * @param tc scheduler context (unused) + */ +static void +context_task (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc) +{ + long timeout; + int max_fd; + fd_set read_fd_set; + fd_set write_fd_set; + fd_set except_fd_set; + struct GNUNET_NETWORK_FDSet *rs; + struct GNUNET_NETWORK_FDSet *ws; + struct GNUNET_TIME_Relative delay; + + ctx_task = NULL; + TALER_BANK_perform (ctx); + max_fd = -1; + timeout = -1; + FD_ZERO (&read_fd_set); + FD_ZERO (&write_fd_set); + FD_ZERO (&except_fd_set); + TALER_BANK_get_select_info (ctx, + &read_fd_set, + &write_fd_set, + &except_fd_set, + &max_fd, + &timeout); + if (timeout >= 0) + delay = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, + timeout); + else + delay = GNUNET_TIME_UNIT_FOREVER_REL; + rs = GNUNET_NETWORK_fdset_create (); + GNUNET_NETWORK_fdset_copy_native (rs, + &read_fd_set, + max_fd + 1); + ws = GNUNET_NETWORK_fdset_create (); + GNUNET_NETWORK_fdset_copy_native (ws, + &write_fd_set, + max_fd + 1); + ctx_task = GNUNET_SCHEDULER_add_select (GNUNET_SCHEDULER_PRIORITY_DEFAULT, + delay, + rs, + ws, + &context_task, + cls); + GNUNET_NETWORK_fdset_destroy (rs); + GNUNET_NETWORK_fdset_destroy (ws); +} + + +/** + * Main function that will be run by the scheduler. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param config configuration + */ +static void +run (void *cls, + const struct GNUNET_SCHEDULER_TaskContext *tc) +{ + struct InterpreterState *is; + static struct Command commands[] = + { + /* Add EUR:5.01 to account 42 */ + { .oc = OC_ADMIN_ADD_INCOMING, + .label = "deposit-1", + .expected_response_code = MHD_HTTP_OK, + .details.admin_add_incoming.account_no = 42, + .details.admin_add_incoming.amount = "EUR:5.01" }, + + { .oc = OC_END } + }; + + is = GNUNET_new (struct InterpreterState); + is->commands = commands; + + ctx = TALER_BANK_init ("http://localhost:8081"); + GNUNET_assert (NULL != ctx); + ctx_task = GNUNET_SCHEDULER_add_now (&context_task, + ctx); + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); + shutdown_task + = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply + (GNUNET_TIME_UNIT_SECONDS, 150), + &do_shutdown, is); +} + + +/** + * Main function for the testcase for the bank API. + * + * @param argc expected to be 1 + * @param argv expected to only contain the program name + */ +int +main (int argc, + char * const *argv) +{ + struct GNUNET_OS_Process *bankd; + + GNUNET_log_setup ("test-bank-api", + "WARNING", + NULL); + bankd = GNUNET_OS_start_process (GNUNET_NO, + GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "taler-bank-httpd", + "taler-bank-httpd", + "-d", "test-bank-home", + NULL); + if (NULL == bankd) + { + fprintf (stderr, + "taler-bank-httpd not found, skipping test\n"); + return 77; /* report 'skip' */ + } + /* give child time to start and bind against the socket */ + fprintf (stderr, + "Waiting for taler-bank-httpd to be ready"); + do + { + fprintf (stderr, "."); + sleep (1); + } + while (0 != system ("wget -q -t 1 -T 1 http://127.0.0.1:8081/keys -o /dev/null -O /dev/null")); + fprintf (stderr, "\n"); + result = GNUNET_SYSERR; + GNUNET_SCHEDULER_run (&run, NULL); + GNUNET_OS_process_kill (bankd, + SIGTERM); + GNUNET_OS_process_wait (bankd); + GNUNET_OS_process_destroy (bankd); + return (GNUNET_OK == result) ? 0 : 1; +} + +/* end of test_bank_api.c */ |