diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/Makefile.am | 47 | ||||
-rw-r--r-- | src/lib/merchant_api_context.c | 525 | ||||
-rw-r--r-- | src/lib/merchant_api_context.h | 169 | ||||
-rw-r--r-- | src/lib/merchant_api_json.c | 491 | ||||
-rw-r--r-- | src/lib/merchant_api_json.h | 331 | ||||
-rw-r--r-- | src/lib/merchant_api_pay.c | 317 | ||||
-rw-r--r-- | src/lib/test_merchant_api.c | 1345 |
7 files changed, 3225 insertions, 0 deletions
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am new file mode 100644 index 00000000..8696db40 --- /dev/null +++ b/src/lib/Makefile.am @@ -0,0 +1,47 @@ +# 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 = \ + libtalermerchant.la + +libtalermerchant_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined + +libtalermerchant_la_SOURCES = \ + merchant_api_context.c merchant_api_context.h \ + merchant_api_json.c merchant_api_json.h \ + merchant_api_pay.c + +libtalermerchant_la_LIBADD = \ + -lgnunetutil \ + -ljansson \ + $(XLIB) + +if HAVE_LIBCURL +libtalermerchant_la_LIBADD += -lcurl +else +if HAVE_LIBGNURL +libtalermerchant_la_LIBADD += -lgnurl +endif +endif + +check_PROGRAMS = \ + test_merchant_api + +TESTS = \ + $(check_PROGRAMS) + +test_merchant_api_SOURCES = \ + test_merchant_api.c +test_merchant_api_LDADD = \ + libtalermerchant.la \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetutil \ + -ljansson diff --git a/src/lib/merchant_api_context.c b/src/lib/merchant_api_context.c new file mode 100644 index 00000000..5b8b9e4f --- /dev/null +++ b/src/lib/merchant_api_context.c @@ -0,0 +1,525 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 Christian Grothoff (and other contributing authors) + + 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 merchant-lib/merchant_api_context.c + * @brief Implementation of the context part of the merchant's HTTP API + * @author Sree Harsha Totakura <sreeharsha@totakura.in> + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include "taler_merchant_service.h" +#include "merchant_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_MERCHANT_curl_fail; + + +/** + * Jobs are CURL requests running within a `struct TALER_MERCHANT_Context`. + */ +struct MAC_Job +{ + + /** + * We keep jobs in a DLL. + */ + struct MAC_Job *next; + + /** + * We keep jobs in a DLL. + */ + struct MAC_Job *prev; + + /** + * Easy handle of the job. + */ + CURL *easy_handle; + + /** + * Context this job runs in. + */ + struct TALER_MERCHANT_Context *ctx; + + /** + * Function to call upon completion. + */ + MAC_JobCompletionCallback jcc; + + /** + * Closure for @e jcc. + */ + void *jcc_cls; + +}; + + +/** + * Context + */ +struct TALER_MERCHANT_Context +{ + /** + * Curl multi handle + */ + CURLM *multi; + + /** + * Curl share handle + */ + CURLSH *share; + + /** + * We keep jobs in a DLL. + */ + struct MAC_Job *jobs_head; + + /** + * We keep jobs in a DLL. + */ + struct MAC_Job *jobs_tail; + + /** + * HTTP header "application/json", created once and used + * for all requests that need it. + */ + struct curl_slist *json_header; + +}; + + +/** + * Initialise this library. This function should be called before using any of + * the following functions. + * + * @return library context + */ +struct TALER_MERCHANT_Context * +TALER_MERCHANT_init () +{ + struct TALER_MERCHANT_Context *ctx; + CURLM *multi; + CURLSH *share; + + if (TALER_MERCHANT_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_MERCHANT_Context); + ctx->multi = multi; + ctx->share = share; + 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 #MAC_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 MAC_Job * +MAC_job_add (struct TALER_MERCHANT_Context *ctx, + CURL *eh, + int add_json, + MAC_JobCompletionCallback jcc, + void *jcc_cls) +{ + struct MAC_Job *job; + + if (GNUNET_YES == add_json) + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HTTPHEADER, + ctx->json_header)); + + job = GNUNET_new (struct MAC_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 #MAC_job_add(). + * + * @param eh easy handle that was used + * @return the `jcc_cls` that was given to #MAC_job_add(). + */ +void * +MAC_easy_to_closure (CURL *eh) +{ + struct MAC_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 +MAC_job_cancel (struct MAC_Job *job) +{ + struct TALER_MERCHANT_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_MERCHANT_perform (struct TALER_MERCHANT_Context *ctx) +{ + CURLMsg *cmsg; + struct MAC_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); + MAC_job_cancel (job); + } +} + + +/** + * Obtain the information for a select() call to wait until + * #TALER_MERCHANT_perform() is ready again. Note that calling + * any other TALER_MERCHANT-API may also imply that the library + * is again ready for #TALER_MERCHANT_perform(). + * + * Basically, a client should use this API to prepare for select(), + * then block on select(), then call #TALER_MERCHANT_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_MERCHANT_perform(). + */ +void +TALER_MERCHANT_get_select_info (struct TALER_MERCHANT_Context *ctx, + fd_set *read_fd_set, + fd_set *write_fd_set, + fd_set *except_fd_set, + int *max_fd, + long *timeout) +{ + GNUNET_assert (CURLM_OK == + curl_multi_fdset (ctx->multi, + read_fd_set, + write_fd_set, + except_fd_set, + max_fd)); + GNUNET_assert (CURLM_OK == + curl_multi_timeout (ctx->multi, + timeout)); + if ( (-1 == (*timeout)) && + (NULL != ctx->jobs_head) ) + *timeout = 1000 * 60 * 5; /* curl is not always good about giving timeouts */ +} + + +/** + * 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_MERCHANT_fini (struct TALER_MERCHANT_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); +} + + +/** + * Callback used when downloading the reply to an HTTP request. + * Just appends all of the data to the `buf` in the + * `struct MAC_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 +MAC_download_cb (char *bufptr, + size_t size, + size_t nitems, + void *cls) +{ + struct MAC_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 * +MAC_download_get_result (struct MAC_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_MERCHANT_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_MERCHANT_curl_fail = 1; + } +} + + +/** + * Cleans up after us, specifically runs the Curl cleanup. + */ +__attribute__ ((destructor)) +void +TALER_MERCHANT_destructor__ (void) +{ + if (TALER_MERCHANT_curl_fail) + return; + curl_global_cleanup (); +} + +/* end of merchant_api_context.c */ diff --git a/src/lib/merchant_api_context.h b/src/lib/merchant_api_context.h new file mode 100644 index 00000000..6a8d7a28 --- /dev/null +++ b/src/lib/merchant_api_context.h @@ -0,0 +1,169 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 Christian Grothoff (and other contributing authors) + + 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 merchant-lib/merchant_api_context.h + * @brief Internal interface to the context part of the merchant'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_merchant_service.h" +#include "taler_signatures.h" + + +/** + * Entry in the context's job queue. + */ +struct MAC_Job; + +/** + * Function to call upon completion of a job. + * + * @param cls closure + * @param eh original easy handle (for inspection) + */ +typedef void +(*MAC_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 #MAC_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 MAC_Job * +MAC_job_add (struct TALER_MERCHANT_Context *ctx, + CURL *eh, + int add_json, + MAC_JobCompletionCallback jcc, + void *jcc_cls); + + +/** + * Obtain the `jcc_cls` argument from an `eh` that was + * given to #MAC_job_add(). + * + * @param eh easy handle that was used + * @return the `jcc_cls` that was given to #MAC_job_add(). + */ +void * +MAC_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 +MAC_job_cancel (struct MAC_Job *job); + + +/** + * @brief Buffer data structure we use to buffer the HTTP download + * before giving it to the JSON parser. + */ +struct MAC_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 MAC_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 MAC_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 +MAC_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 * +MAC_download_get_result (struct MAC_DownloadBuffer *db, + CURL *eh, + long *response_code); + + +/* end of merchant_api_context.h */ diff --git a/src/lib/merchant_api_json.c b/src/lib/merchant_api_json.c new file mode 100644 index 00000000..9d03db65 --- /dev/null +++ b/src/lib/merchant_api_json.c @@ -0,0 +1,491 @@ +/* + 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 lib/merchant_api_json.c + * @brief functions to parse incoming requests (JSON snippets) + * @author Florian Dold + * @author Benedikt Mueller + * @author Christian Grothoff + */ +#include "platform.h" +#include "merchant_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 MAJ_Specification *spec) +{ + int i; + json_t *pos; /* what's our current position? */ + + pos = root; + for (i=0;MAJ_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 MAJ_CMD_END: + GNUNET_assert (0); + return i; + case MAJ_CMD_AMOUNT: + if (GNUNET_OK != + TALER_json_to_amount (pos, + spec[i].details.amount)) + { + GNUNET_break_op (0); + return i; + } + break; + case MAJ_CMD_TIME_ABSOLUTE: + if (GNUNET_OK != + TALER_json_to_abs (pos, + spec[i].details.abs_time)) + { + GNUNET_break_op (0); + return i; + } + break; + + case MAJ_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 MAJ_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 MAJ_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 MAJ_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 MAJ_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 MAJ_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 MAJ_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 + * #MAJ_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 MAJ_Specification *spec, + int end) +{ + int i; + + for (i=0;i<end;i++) + { + switch (spec[i].cmd) + { + case MAJ_CMD_END: + GNUNET_assert (0); + return; + case MAJ_CMD_AMOUNT: + break; + case MAJ_CMD_TIME_ABSOLUTE: + break; + case MAJ_CMD_BINARY_FIXED: + break; + case MAJ_CMD_STRING: + break; + case MAJ_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 MAJ_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 MAJ_CMD_RSA_SIGNATURE: + GNUNET_CRYPTO_rsa_signature_free (*spec[i].details.rsa_signature); + *spec[i].details.rsa_signature = NULL; + break; + case MAJ_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 +MAJ_parse_json (const json_t *root, + struct MAJ_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 + * #MAJ_parse_json() operation. + * + * @param spec specification of the parse operation + */ +void +MAJ_parse_free (struct MAJ_Specification *spec) +{ + int i; + + for (i=0;MAJ_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 MAJ_Specification +MAJ_spec_string (const char *name, + const char **strptr) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_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 MAJ_Specification +MAJ_spec_absolute_time (const char *name, + struct GNUNET_TIME_Absolute *at) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_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 MAJ_Specification +MAJ_spec_amount (const char *name, + struct TALER_Amount *amount) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_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 MAJ_Specification +MAJ_spec_uint16 (const char *name, + uint16_t *u16) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_CMD_UINT16, + .field = name, + .details.u16 = u16 + }; + return ret; +} + + +/** + * JSON object. + * + * @param name name of the JSON field + * @param[out] jsonp where to store the JSON found under @a name + */ +struct MAJ_Specification +MAJ_spec_json (const char *name, + json_t **jsonp) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_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 MAJ_Specification +MAJ_spec_rsa_public_key (const char *name, + struct GNUNET_CRYPTO_rsa_PublicKey **pk) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_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 MAJ_Specification +MAJ_spec_rsa_signature (const char *name, + struct GNUNET_CRYPTO_rsa_Signature **sig) +{ + struct MAJ_Specification ret = + { + .cmd = MAJ_CMD_RSA_SIGNATURE, + .field = name, + .details.rsa_signature = sig + }; + return ret; +} + + +/* end of merchant_api_json.c */ diff --git a/src/lib/merchant_api_json.h b/src/lib/merchant_api_json.h new file mode 100644 index 00000000..9f7b0812 --- /dev/null +++ b/src/lib/merchant_api_json.h @@ -0,0 +1,331 @@ +/* + 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 lib/merchant_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 + * #MAJ_parse_json interpreter. + */ +enum MAJ_Command +{ + + /** + * End of command list. + */ + MAJ_CMD_END, + + /** + * Parse amount at current position. + */ + MAJ_CMD_AMOUNT, + + /** + * Parse absolute time at current position. + */ + MAJ_CMD_TIME_ABSOLUTE, + + /** + * Parse fixed binary value at current position. + */ + MAJ_CMD_BINARY_FIXED, + + /** + * Parse variable-size binary value at current position. + */ + MAJ_CMD_BINARY_VARIABLE, + + /** + * Parse RSA public key at current position. + */ + MAJ_CMD_RSA_PUBLIC_KEY, + + /** + * Parse RSA signature at current position. + */ + MAJ_CMD_RSA_SIGNATURE, + + /** + * Parse `const char *` JSON string at current position. + */ + MAJ_CMD_STRING, + + /** + * Parse `uint16_t` integer at the current position. + */ + MAJ_CMD_UINT16, + + /** + * Parse JSON object at the current position. + */ + MAJ_CMD_JSON_OBJECT, + + /** + * Parse ??? at current position. + */ + MAJ_CMD_C + +}; + + +/** + * @brief Entry in parser specification for #MAJ_parse_json. + */ +struct MAJ_Specification +{ + + /** + * Command to execute. + */ + enum MAJ_Command cmd; + + /** + * Name of the field to access. + */ + const char *field; + + /** + * Further details for the command. + */ + union { + + /** + * Where to store amount for #MAJ_CMD_AMOUNT. + */ + struct TALER_Amount *amount; + + /** + * Where to store time, for #MAJ_CMD_TIME_ABSOLUTE. + */ + struct GNUNET_TIME_Absolute *abs_time; + + /** + * Where to write binary data, for #MAJ_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 #MAJ_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 #MAJ_CMD_RSA_PUBLIC_KEY + */ + struct GNUNET_CRYPTO_rsa_PublicKey **rsa_public_key; + + /** + * Where to store the RSA signature for #MAJ_CMD_RSA_SIGNATURE + */ + struct GNUNET_CRYPTO_rsa_Signature **rsa_signature; + + /** + * Details for #MAJ_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 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 +MAJ_parse_json (const json_t *root, + struct MAJ_Specification *spec); + + +/** + * Free all elements allocated during a + * #MAJ_parse_json() operation. + * + * @param spec specification of the parse operation + */ +void +MAJ_parse_free (struct MAJ_Specification *spec); + + +/** + * End of a parser specification. + */ +#define MAJ_spec_end { .cmd = MAJ_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 MAJ_spec_fixed_auto(name,obj) { .cmd = MAJ_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 MAJ_spec_varsize(name,obj,size) { .cmd = MAJ_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 MAJ_Specification +MAJ_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 MAJ_Specification +MAJ_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 MAJ_Specification +MAJ_spec_uint16 (const char *name, + uint16_t *u16); + + +/** + * JSON object. + * + * @param name name of the JSON field + * @param[out] jsonp where to store the JSON found under @a name + */ +struct MAJ_Specification +MAJ_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 MAJ_Specification +MAJ_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 MAJ_Specification +MAJ_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 MAJ_Specification +MAJ_spec_rsa_signature (const char *name, + struct GNUNET_CRYPTO_rsa_Signature **sig); + + + + +/* end of merchant_api_json.h */ diff --git a/src/lib/merchant_api_pay.c b/src/lib/merchant_api_pay.c new file mode 100644 index 00000000..13715f85 --- /dev/null +++ b/src/lib/merchant_api_pay.c @@ -0,0 +1,317 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 Christian Grothoff (and other contributing authors) + + 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 lib/merchant_api_pay.c + * @brief Implementation of the /pay request of the merchant's HTTP API + * @author Christian Grothoff + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_json.h" +#include "merchant_api_context.h" +#include "taler_signatures.h" + + +/** + * @brief A Pay Handle + */ +struct TALER_MERCHANT_Pay +{ + + /** + * The url for this request. + */ + char *url; + + /** + * JSON encoding of the request to POST. + */ + char *json_enc; + + /** + * Handle for the request. + */ + struct MAC_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_PayCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Download buffer + */ + struct MAC_DownloadBuffer db; + +}; + + + +/** + * Function called when we're done processing the + * HTTP /pay request. + * + * @param cls the `struct TALER_MERCHANT_PayHandle` + * @param eh the curl request handle + */ +static void +handle_pay_finished (void *cls, + CURL *eh) +{ + struct TALER_MERCHANT_PayHandle *ph = cls; + long response_code; + json_t *json; + + ph->job = NULL; + json = MAC_download_get_result (&ph->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 merchant is buggy + (or API version conflict); just pass JSON reply to the application */ + break; + case MHD_HTTP_FORBIDDEN: + break; + case MHD_HTTP_UNAUTHORIZED: + /* Nothing really to verify, merchant says one of the signatures is + invalid; as we checked them, this should never happen, we + should pass the JSON reply to the application */ + break; + case MHD_HTTP_NOT_FOUND: + /* Nothing really to verify, this should never + happen, we should pass the JSON reply to the application */ + break; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + /* Server had an internal issue; we should retry, but this API + leaves this to the application */ + break; + default: + /* unexpected response code */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u\n", + response_code); + GNUNET_break (0); + response_code = 0; + break; + } + ph->cb (ph->cb_cls, + response_code, + json); + json_decref (json); + TALER_MERCHANT_pay_cancel (ph); +} + + +/** + * Pay a merchant. API for wallets that have the coin's private keys. + * + * @param merchant the merchant context + * @param h_wire hash of the merchant’s account details + * @param h_contract hash of the contact of the merchant with the customer + * @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant + * @param transaction_id transaction id for the transaction between merchant and customer + * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) + * @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed) + * @param num_coins number of coins used to pay + * @param coins array of coins we use to pay + * @param coin_sig the signature made with purpose #TALER_SIGNATURE_WALLET_COIN_DEPOSIT made by the customer with the coin’s private key. + * @param pay_cb the callback to call when a reply for this request is available + * @param pay_cb_cls closure for @a pay_cb + * @return a handle for this request + */ +struct TALER_MERCHANT_Pay * +TALER_MERCHANT_pay_wallet (struct TALER_MERCHANT_Context *merchant, + const struct GNUNET_HashCode *h_wire, + const struct GNUNET_HashCode *h_contract, + struct GNUNET_TIME_Absolute timestamp, + uint64_t transaction_id, + const struct TALER_MerchantPublicKeyP *merchant_pub, + struct GNUNET_TIME_Absolute refund_deadline, + unsigned int num_coins, + const struct TLAER_MERCHANT_PayCoin *coins, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls) +{ + struct TALER_MERCHANT_PayHandle *ph; + json_t *pay_obj; + CURL *eh; + struct GNUNET_HashCode h_wire; + struct TALER_Amount amount_without_fee; + + pay_obj = json_pack ("{s:o, s:O," /* f/wire */ + " s:o, s:o," /* H_wire, H_contract */ + " s:o, s:o," /* coin_pub, denom_pub */ + " s:o, s:o," /* ub_sig, timestamp */ + " s:I, s:o," /* transaction id, merchant_pub */ + " s:o, s:o," /* refund_deadline, wire_deadline */ + " s:o}", /* coin_sig */ + "f", TALER_json_from_amount (amount), + "wire", wire_details, + "H_wire", TALER_json_from_data (&h_wire, + sizeof (h_wire)), + "H_contract", TALER_json_from_data (h_contract, + sizeof (struct GNUNET_HashCode)), + "coin_pub", TALER_json_from_data (coin_pub, + sizeof (*coin_pub)), + "denom_pub", TALER_json_from_rsa_public_key (denom_pub->rsa_public_key), + "ub_sig", TALER_json_from_rsa_signature (denom_sig->rsa_signature), + "timestamp", TALER_json_from_abs (timestamp), + "transaction_id", (json_int_t) transaction_id, + "merchant_pub", TALER_json_from_data (merchant_pub, + sizeof (*merchant_pub)), + "refund_deadline", TALER_json_from_abs (refund_deadline), + "edate", TALER_json_from_abs (wire_deadline), + "coin_sig", TALER_json_from_data (coin_sig, + sizeof (*coin_sig)) + ); + + ph = GNUNET_new (struct TALER_MERCHANT_PayHandle); +#if 0 + ph->merchant = merchant; + ph->cb = cb; + ph->cb_cls = cb_cls; + ph->url = MAH_path_to_url (merchant, "/pay"); + ph->depconf.purpose.size = htonl (sizeof (struct TALER_PayConfirmationPS)); + ph->depconf.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONFIRM_PAY); + ph->depconf.h_contract = *h_contract; + ph->depconf.h_wire = h_wire; + ph->depconf.transaction_id = GNUNET_htonll (transaction_id); + ph->depconf.timestamp = GNUNET_TIME_absolute_hton (timestamp); + ph->depconf.refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline); + TALER_amount_subtract (&amount_without_fee, + amount, + &dki->fee_pay); + TALER_amount_hton (&ph->depconf.amount_without_fee, + &amount_without_fee); + ph->depconf.coin_pub = *coin_pub; + ph->depconf.merchant = *merchant_pub; + ph->amount_with_fee = *amount; + ph->coin_value = dki->value; + + eh = curl_easy_init (); + GNUNET_assert (NULL != (ph->json_enc = + json_dumps (pay_obj, + JSON_COMPACT))); + json_decref (pay_obj); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ph->url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + ph->json_enc)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDSIZE, + strlen (ph->json_enc))); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_WRITEFUNCTION, + &MAC_download_cb)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_WRITEDATA, + &ph->db)); + ctx = MAH_handle_to_context (merchant); + ph->job = MAC_job_add (ctx, + eh, + GNUNET_YES, + &handle_pay_finished, + ph); + return ph; +#endif + return NULL; +} + + + +/** + * Pay a merchant. API for frontends talking to backends. Here, + * the frontend does not have the coin's private keys, but just + * the public keys and signatures. Note the sublte difference + * in the type of @a coins compared to #TALER_MERCHANT_pay(). + * + * @param merchant the merchant context + * @param h_wire hash of the merchant’s account details + * @param h_contract hash of the contact of the merchant with the customer + * @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant + * @param transaction_id transaction id for the transaction between merchant and customer + * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests) + * @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed) + * @param num_coins number of coins used to pay + * @param coins array of coins we use to pay + * @param coin_sig the signature made with purpose #TALER_SIGNATURE_WALLET_COIN_DEPOSIT made by the customer with the coin’s private key. + * @param pay_cb the callback to call when a reply for this request is available + * @param pay_cb_cls closure for @a pay_cb + * @return a handle for this request + */ +struct TALER_MERCHANT_Pay * +TALER_MERCHANT_pay_frontend (struct TALER_MERCHANT_Context *merchant, + const struct GNUNET_HashCode *h_wire, + const struct GNUNET_HashCode *h_contract, + struct GNUNET_TIME_Absolute timestamp, + uint64_t transaction_id, + const struct TALER_MerchantPublicKeyP *merchant_pub, + struct GNUNET_TIME_Absolute refund_deadline, + unsigned int num_coins, + const struct TALER_MERCHANT_PaidCoin *coins, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls) +{ + GNUNET_break (0); // FIXME: not implemented! + return NULL; +} + + +/** + * Cancel a pay permission request. This function cannot be used + * on a request handle if a response is already served for it. + * + * @param pay the pay permission request handle + */ +void +TALER_MERCHANT_pay_cancel (struct TALER_MERCHANT_PayHandle *pay) +{ + if (NULL != pay->job) + { + MAC_job_cancel (pay->job); + pay->job = NULL; + } + GNUNET_free_non_null (pay->db.buf); + GNUNET_free (pay->url); + GNUNET_free (pay->json_enc); + GNUNET_free (pay); +} + + +/* end of merchant_api_pay.c */ diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c new file mode 100644 index 00000000..c264f9a3 --- /dev/null +++ b/src/lib/test_merchant_api.c @@ -0,0 +1,1345 @@ +/* + This file is part of TALER + Copyright (C) 2014, 2015 Christian Grothoff (and other contributing authors) + + 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 merchant/test_merchant_api.c + * @brief testcase to test merchant's HTTP API interface + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_util.h" +#include "taler_signatures.h" +#include "taler_mint_service.h" +#include "taler_merchant_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> + +/** + * Main execution context for the main loop of the mint. + */ +static struct TALER_MINT_Context *ctx; + +/** + * Handle to access the mint. + */ +static struct TALER_MINT_Handle *mint; + +/** + * Main execution context for the main loop of the mint. + */ +static struct TALER_MERCHANT_Context *merchant; + +/** + * 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, + + /** + * Check status of a reserve. + */ + OC_WITHDRAW_STATUS, + + /** + * Withdraw a coin from a reserve. + */ + OC_WITHDRAW_SIGN, + + /** + * Pay with coins. + */ + OC_PAY + +}; + + +/** + * Structure specifying details about a coin to be melted. + * Used in a NULL-terminated array as part of command + * specification. + */ +struct MeltDetails +{ + + /** + * Amount to melt (including fee). + */ + const char *amount; + + /** + * Reference to reserve_withdraw operations for coin to + * be used for the /refresh/melt operation. + */ + const char *coin_ref; + +}; + + +/** + * Information about a fresh coin generated by the refresh operation. + */ +struct FreshCoin +{ + + /** + * If @e amount is NULL, this specifies the denomination key to + * use. Otherwise, this will be set (by the interpreter) to the + * denomination PK matching @e amount. + */ + const struct TALER_MINT_DenomPublicKey *pk; + + /** + * Set (by the interpreter) to the mint's signature over the + * coin's public key. + */ + struct TALER_DenominationSignature sig; + + /** + * Set (by the interpreter) to the coin's private key. + */ + struct TALER_CoinSpendPrivateKeyP coin_priv; + +}; + + +/** + * Details for a mint operation to execute. + */ +struct Command +{ + /** + * Opcode of the command. + */ + enum OpCode oc; + + /** + * Label for the command, can be NULL. + */ + const char *label; + + /** + * Which response code do we expect for this command? + */ + unsigned int expected_response_code; + + /** + * Details about the command. + */ + union + { + + /** + * Information for a #OC_ADMIN_ADD_INCOMING command. + */ + struct + { + + /** + * Label to another admin_add_incoming command if we + * should deposit into an existing reserve, NULL if + * a fresh reserve should be created. + */ + const char *reserve_reference; + + /** + * String describing the amount to add to the reserve. + */ + const char *amount; + + /** + * Wire details (JSON). + */ + const char *wire; + + /** + * Set (by the interpreter) to the reserve's private key + * we used to fill the reserve. + */ + struct TALER_ReservePrivateKeyP reserve_priv; + + /** + * Set to the API's handle during the operation. + */ + struct TALER_MINT_AdminAddIncomingHandle *aih; + + } admin_add_incoming; + + /** + * Information for a #OC_WITHDRAW_STATUS command. + */ + struct + { + + /** + * Label to the #OC_ADMIN_ADD_INCOMING command which + * created the reserve. + */ + const char *reserve_reference; + + /** + * Set to the API's handle during the operation. + */ + struct TALER_MINT_ReserveStatusHandle *wsh; + + /** + * Expected reserve balance. + */ + const char *expected_balance; + + } reserve_status; + + /** + * Information for a #OC_WITHDRAW_SIGN command. + */ + struct + { + + /** + * Which reserve should we withdraw from? + */ + const char *reserve_reference; + + /** + * String describing the denomination value we should withdraw. + * A corresponding denomination key must exist in the mint's + * offerings. Can be NULL if @e pk is set instead. + */ + const char *amount; + + /** + * If @e amount is NULL, this specifies the denomination key to + * use. Otherwise, this will be set (by the interpreter) to the + * denomination PK matching @e amount. + */ + const struct TALER_MINT_DenomPublicKey *pk; + + /** + * Set (by the interpreter) to the mint's signature over the + * coin's public key. + */ + struct TALER_DenominationSignature sig; + + /** + * Set (by the interpreter) to the coin's private key. + */ + struct TALER_CoinSpendPrivateKeyP coin_priv; + + /** + * Blinding key used for the operation. + */ + struct TALER_DenominationBlindingKey blinding_key; + + /** + * Withdraw handle (while operation is running). + */ + struct TALER_MINT_ReserveWithdrawHandle *wsh; + + } reserve_withdraw; + + /** + * Information for a #OC_DEPOSIT command. + */ + struct + { + + /** + * Amount to pay. + */ + const char *amount; + + /** + * Reference to a reserve_withdraw operation for a coin to + * be used for the /deposit operation. + */ + const char *coin_ref; + + /** + * If this @e coin_ref refers to an operation that generated + * an array of coins, this value determines which coin to use. + */ + unsigned int coin_idx; + + /** + * JSON string describing the merchant's "wire details". + */ + const char *wire_details; + + /** + * JSON string describing the contract between the two parties. + */ + const char *contract; + + /** + * Transaction ID to use. + */ + uint64_t transaction_id; + + /** + * Relative time (to add to 'now') to compute the refund deadline. + * Zero for no refunds. + */ + struct GNUNET_TIME_Relative refund_deadline; + + /** + * Set (by the interpreter) to a fresh private key of the merchant, + * if @e refund_deadline is non-zero. + */ + struct TALER_MerchantPrivateKeyP merchant_priv; + + /** + * Deposit handle while operation is running. + */ + struct TALER_MINT_DepositHandle *dh; + + } pay; + + } details; + +}; + + +/** + * State of the interpreter loop. + */ +struct InterpreterState +{ + /** + * Keys from the mint. + */ + const struct TALER_MINT_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 (); +} + + +/** + * Find a command by label. + * + * @param is interpreter state to search + * @param label label to look for + * @return NULL if command was not found + */ +static const struct Command * +find_command (const struct InterpreterState *is, + const char *label) +{ + unsigned int i; + const struct Command *cmd; + + if (NULL == label) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Attempt to lookup command for empty label\n"); + return NULL; + } + for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) + if ( (NULL != cmd->label) && + (0 == strcmp (cmd->label, + label)) ) + return cmd; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command not found: %s\n", + label); + return NULL; +} + + +/** + * Run the main interpreter loop that performs mint 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 mint's reply is bogus (fails to follow the protocol) + * @param full_response full response from the mint (for logging, in case of errors) + */ +static void +add_incoming_cb (void *cls, + unsigned int http_status, + json_t *full_response) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + + cmd->details.admin_add_incoming.aih = NULL; + if (MHD_HTTP_OK != http_status) + { + GNUNET_break (0); + fail (is); + return; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * Check if the given historic event @a h corresponds to the given + * command @a cmd. + * + * @param h event in history + * @param cmd an #OC_ADMIN_ADD_INCOMING command + * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not + */ +static int +compare_admin_add_incoming_history (const struct TALER_MINT_ReserveHistory *h, + const struct Command *cmd) +{ + struct TALER_Amount amount; + + if (TALER_MINT_RTT_DEPOSIT != h->type) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.admin_add_incoming.amount, + &amount)); + if (0 != TALER_amount_cmp (&amount, + &h->amount)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Check if the given historic event @a h corresponds to the given + * command @a cmd. + * + * @param h event in history + * @param cmd an #OC_WITHDRAW_SIGN command + * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not + */ +static int +compare_reserve_withdraw_history (const struct TALER_MINT_ReserveHistory *h, + const struct Command *cmd) +{ + struct TALER_Amount amount; + struct TALER_Amount amount_with_fee; + + if (TALER_MINT_RTT_WITHDRAWAL != h->type) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.reserve_withdraw.amount, + &amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&amount_with_fee, + &amount, + &cmd->details.reserve_withdraw.pk->fee_withdraw)); + if (0 != TALER_amount_cmp (&amount_with_fee, + &h->amount)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Function called with the result of a /reserve/status request. + * + * @param cls closure with the interpreter state + * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request + * 0 if the mint's reply is bogus (fails to follow the protocol) + * @param[in] json original response in JSON format (useful only for diagnostics) + * @param balance current balance in the reserve, NULL on error + * @param history_length number of entries in the transaction history, 0 on error + * @param history detailed transaction history, NULL on error + */ +static void +reserve_status_cb (void *cls, + unsigned int http_status, + json_t *json, + const struct TALER_Amount *balance, + unsigned int history_length, + const struct TALER_MINT_ReserveHistory *history) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + struct Command *rel; + unsigned int i; + unsigned int j; + struct TALER_Amount amount; + + cmd->details.reserve_status.wsh = NULL; + if (cmd->expected_response_code != http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s\n", + http_status, + cmd->label); + GNUNET_break (0); + json_dumpf (json, stderr, 0); + fail (is); + return; + } + switch (http_status) + { + case MHD_HTTP_OK: + /* FIXME: note that history events may come in a different + order than the commands. However, for now this works... */ + j = 0; + for (i=0;i<is->ip;i++) + { + switch ((rel = &is->commands[i])->oc) + { + case OC_ADMIN_ADD_INCOMING: + if ( ( (NULL != rel->label) && + (0 == strcmp (cmd->details.reserve_status.reserve_reference, + rel->label) ) ) || + ( (NULL != rel->details.admin_add_incoming.reserve_reference) && + (0 == strcmp (cmd->details.reserve_status.reserve_reference, + rel->details.admin_add_incoming.reserve_reference) ) ) ) + { + if (GNUNET_OK != + compare_admin_add_incoming_history (&history[j], + rel)) + { + GNUNET_break (0); + fail (is); + return; + } + j++; + } + break; + case OC_WITHDRAW_SIGN: + if (0 == strcmp (cmd->details.reserve_status.reserve_reference, + rel->details.reserve_withdraw.reserve_reference)) + { + if (GNUNET_OK != + compare_reserve_withdraw_history (&history[j], + rel)) + { + GNUNET_break (0); + fail (is); + return; + } + j++; + } + break; + default: + /* unreleated, just skip */ + break; + } + } + if (j != history_length) + { + GNUNET_break (0); + fail (is); + return; + } + if (NULL != cmd->details.reserve_status.expected_balance) + { + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.reserve_status.expected_balance, + &amount)); + if (0 != TALER_amount_cmp (&amount, + balance)) + { + GNUNET_break (0); + fail (is); + return; + } + } + break; + default: + /* Unsupported status code (by test harness) */ + GNUNET_break (0); + break; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * Function called upon completion of our /reserve/withdraw request. + * + * @param cls closure with the interpreter state + * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request + * 0 if the mint's reply is bogus (fails to follow the protocol) + * @param sig signature over the coin, NULL on error + * @param full_response full response from the mint (for logging, in case of errors) + */ +static void +reserve_withdraw_cb (void *cls, + unsigned int http_status, + const struct TALER_DenominationSignature *sig, + json_t *full_response) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + + cmd->details.reserve_withdraw.wsh = NULL; + if (cmd->expected_response_code != http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s\n", + http_status, + cmd->label); + json_dumpf (full_response, stderr, 0); + GNUNET_break (0); + fail (is); + return; + } + switch (http_status) + { + case MHD_HTTP_OK: + if (NULL == sig) + { + GNUNET_break (0); + fail (is); + return; + } + cmd->details.reserve_withdraw.sig.rsa_signature + = GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature); + break; + case MHD_HTTP_PAYMENT_REQUIRED: + /* nothing to check */ + break; + default: + /* Unsupported status code (by test harness) */ + GNUNET_break (0); + break; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * Function called with the result of a /pay operation. + * + * @param cls closure with the interpreter state + * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit; + * 0 if the mint's reply is bogus (fails to follow the protocol) + * @param obj the received JSON reply, should be kept as proof (and, in case of errors, + * be forwarded to the customer) + */ +static void +pay_cb (void *cls, + unsigned int http_status, + const char *redirect_uri, + json_t *obj) +{ + struct InterpreterState *is = cls; + struct Command *cmd = &is->commands[is->ip]; + + cmd->details.deposit.dh = NULL; + if (cmd->expected_response_code != http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s\n", + http_status, + cmd->label); + json_dumpf (obj, stderr, 0); + fail (is); + return; + } + is->ip++; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * Find denomination key matching the given amount. + * + * @param keys array of keys to search + * @param amount coin value to look for + * @return NULL if no matching key was found + */ +static const struct TALER_MINT_DenomPublicKey * +find_pk (const struct TALER_MINT_Keys *keys, + const struct TALER_Amount *amount) +{ + unsigned int i; + struct GNUNET_TIME_Absolute now; + struct TALER_MINT_DenomPublicKey *pk; + char *str; + + now = GNUNET_TIME_absolute_get (); + for (i=0;i<keys->num_denom_keys;i++) + { + pk = &keys->denom_keys[i]; + if ( (0 == TALER_amount_cmp (amount, + &pk->value)) && + (now.abs_value_us >= pk->valid_from.abs_value_us) && + (now.abs_value_us < pk->withdraw_valid_until.abs_value_us) ) + return pk; + } + /* do 2nd pass to check if expiration times are to blame for failure */ + str = TALER_amount_to_string (amount); + for (i=0;i<keys->num_denom_keys;i++) + { + pk = &keys->denom_keys[i]; + if ( (0 == TALER_amount_cmp (amount, + &pk->value)) && + ( (now.abs_value_us < pk->valid_from.abs_value_us) || + (now.abs_value_us > pk->withdraw_valid_until.abs_value_us) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Have denomination key for `%s', but with wrong expiration range %llu vs [%llu,%llu)\n", + str, + now.abs_value_us, + pk->valid_from.abs_value_us, + pk->withdraw_valid_until.abs_value_us); + GNUNET_free (str); + return NULL; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No denomination key for amount %s found\n", + str); + GNUNET_free (str); + return NULL; +} + + +/** + * Run the main interpreter loop that performs mint 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]; + const struct Command *ref; + struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_Amount amount; + struct GNUNET_TIME_Absolute execution_date; + json_t *wire; + + 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 (NULL != + cmd->details.admin_add_incoming.reserve_reference) + { + ref = find_command (is, + cmd->details.admin_add_incoming.reserve_reference); + GNUNET_assert (NULL != ref); + GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); + cmd->details.admin_add_incoming.reserve_priv + = ref->details.admin_add_incoming.reserve_priv; + } + else + { + struct GNUNET_CRYPTO_EddsaPrivateKey *priv; + + priv = GNUNET_CRYPTO_eddsa_key_create (); + cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *priv; + GNUNET_free (priv); + } + GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv, + &reserve_pub.eddsa_pub); + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.admin_add_incoming.amount, + &amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse amount `%s' at %u\n", + cmd->details.admin_add_incoming.amount, + is->ip); + fail (is); + return; + } + wire = json_loads (cmd->details.admin_add_incoming.wire, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == wire) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse wire details `%s' at %u\n", + cmd->details.admin_add_incoming.wire, + is->ip); + fail (is); + return; + } + execution_date = GNUNET_TIME_absolute_get (); + TALER_round_abs_time (&execution_date); + cmd->details.admin_add_incoming.aih + = TALER_MINT_admin_add_incoming (mint, + &reserve_pub, + &amount, + execution_date, + wire, + &add_incoming_cb, + is); + if (NULL == cmd->details.admin_add_incoming.aih) + { + GNUNET_break (0); + fail (is); + return; + } + trigger_context_task (); + return; + case OC_WITHDRAW_STATUS: + GNUNET_assert (NULL != + cmd->details.reserve_status.reserve_reference); + ref = find_command (is, + cmd->details.reserve_status.reserve_reference); + GNUNET_assert (NULL != ref); + GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); + GNUNET_CRYPTO_eddsa_key_get_public (&ref->details.admin_add_incoming.reserve_priv.eddsa_priv, + &reserve_pub.eddsa_pub); + cmd->details.reserve_status.wsh + = TALER_MINT_reserve_status (mint, + &reserve_pub, + &reserve_status_cb, + is); + trigger_context_task (); + return; + case OC_WITHDRAW_SIGN: + GNUNET_assert (NULL != + cmd->details.reserve_withdraw.reserve_reference); + ref = find_command (is, + cmd->details.reserve_withdraw.reserve_reference); + GNUNET_assert (NULL != ref); + GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); + if (NULL != cmd->details.reserve_withdraw.amount) + { + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.reserve_withdraw.amount, + &amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse amount `%s' at %u\n", + cmd->details.reserve_withdraw.amount, + is->ip); + fail (is); + return; + } + cmd->details.reserve_withdraw.pk = find_pk (is->keys, + &amount); + } + if (NULL == cmd->details.reserve_withdraw.pk) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to determine denomination key at %u\n", + is->ip); + fail (is); + return; + } + + /* create coin's private key */ + { + struct GNUNET_CRYPTO_EddsaPrivateKey *priv; + + priv = GNUNET_CRYPTO_eddsa_key_create (); + cmd->details.reserve_withdraw.coin_priv.eddsa_priv = *priv; + GNUNET_free (priv); + } + GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.reserve_withdraw.coin_priv.eddsa_priv, + &coin_pub.eddsa_pub); + cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key + = GNUNET_CRYPTO_rsa_blinding_key_create (GNUNET_CRYPTO_rsa_public_key_len (cmd->details.reserve_withdraw.pk->key.rsa_public_key)); + cmd->details.reserve_withdraw.wsh + = TALER_MINT_reserve_withdraw (mint, + cmd->details.reserve_withdraw.pk, + &ref->details.admin_add_incoming.reserve_priv, + &cmd->details.reserve_withdraw.coin_priv, + &cmd->details.reserve_withdraw.blinding_key, + &reserve_withdraw_cb, + is); + if (NULL == cmd->details.reserve_withdraw.wsh) + { + GNUNET_break (0); + fail (is); + return; + } + trigger_context_task (); + return; + case OC_PAY: + { + GNUNET_break (0); // FIXME: not implemented! + 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; + } + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * 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_MINT_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih); + cmd->details.admin_add_incoming.aih = NULL; + } + break; + case OC_WITHDRAW_STATUS: + if (NULL != cmd->details.reserve_status.wsh) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command %u (%s) did not complete\n", + i, + cmd->label); + TALER_MINT_reserve_status_cancel (cmd->details.reserve_status.wsh); + cmd->details.reserve_status.wsh = NULL; + } + break; + case OC_WITHDRAW_SIGN: + if (NULL != cmd->details.reserve_withdraw.wsh) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command %u (%s) did not complete\n", + i, + cmd->label); + TALER_MINT_reserve_withdraw_cancel (cmd->details.reserve_withdraw.wsh); + cmd->details.reserve_withdraw.wsh = NULL; + } + if (NULL != cmd->details.reserve_withdraw.sig.rsa_signature) + { + GNUNET_CRYPTO_rsa_signature_free (cmd->details.reserve_withdraw.sig.rsa_signature); + cmd->details.reserve_withdraw.sig.rsa_signature = NULL; + } + if (NULL != cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key) + { + GNUNET_CRYPTO_rsa_blinding_key_free (cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key); + cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key = NULL; + } + break; + case OC_PAY: + GNUNET_break (0); // FIXME: not implemented + 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 != mint) + { + TALER_MINT_disconnect (mint); + mint = NULL; + } + if (NULL != ctx) + { + TALER_MINT_fini (ctx); + ctx = NULL; + } +} + + +/** + * Functions of this type are called to provide the retrieved signing and + * denomination keys of the mint. No TALER_MINT_*() functions should be called + * in this callback. + * + * @param cls closure + * @param keys information about keys of the mint + */ +static void +cert_cb (void *cls, + const struct TALER_MINT_Keys *keys) +{ + struct InterpreterState *is = cls; + + /* check that keys is OK */ +#define ERR(cond) do { if(!(cond)) break; GNUNET_break (0); GNUNET_SCHEDULER_shutdown(); return; } while (0) + ERR (NULL == keys); + ERR (0 == keys->num_sign_keys); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Read %u signing keys\n", + keys->num_sign_keys); + ERR (0 == keys->num_denom_keys); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Read %u denomination keys\n", + keys->num_denom_keys); +#undef ERR + + /* run actual tests via interpreter-loop */ + is->keys = keys; + is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, + is); +} + + +/** + * 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_MINT_perform (ctx); + max_fd = -1; + timeout = -1; + FD_ZERO (&read_fd_set); + FD_ZERO (&write_fd_set); + FD_ZERO (&except_fd_set); + TALER_MINT_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[] = + { + /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config */ + { .oc = OC_ADMIN_ADD_INCOMING, + .label = "create-reserve-1", + .expected_response_code = MHD_HTTP_OK, + .details.admin_add_incoming.wire = "{ \"type\":\"TEST\", \"bank\":\"source bank\", \"account\":42 }", + .details.admin_add_incoming.amount = "EUR:5.01" }, + /* Withdraw a 5 EUR coin, at fee of 1 ct */ + { .oc = OC_WITHDRAW_SIGN, + .label = "withdraw-coin-1", + .expected_response_code = MHD_HTTP_OK, + .details.reserve_withdraw.reserve_reference = "create-reserve-1", + .details.reserve_withdraw.amount = "EUR:5" }, + /* Check that deposit and withdraw operation are in history, and + that the balance is now at zero */ + { .oc = OC_WITHDRAW_STATUS, + .label = "withdraw-status-1", + .expected_response_code = MHD_HTTP_OK, + .details.reserve_status.reserve_reference = "create-reserve-1", + .details.reserve_status.expected_balance = "EUR:0" }, + /* Try to pay with the 5 EUR coin (in full) */ + { .oc = OC_PAY, + .label = "deposit-simple", + .expected_response_code = MHD_HTTP_OK, + .details.deposit.amount = "EUR:5", + .details.deposit.coin_ref = "withdraw-coin-1", + .details.deposit.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.deposit.contract = "{ \"items\"={ \"name\":\"ice cream\", \"value\":1 } }", + .details.deposit.transaction_id = 1 }, + + /* Try to double-spend the 5 EUR coin with different wire details */ + { .oc = OC_PAY, + .label = "deposit-double-1", + .expected_response_code = MHD_HTTP_FORBIDDEN, + .details.deposit.amount = "EUR:5", + .details.deposit.coin_ref = "withdraw-coin-1", + .details.deposit.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":43 }", + .details.deposit.contract = "{ \"items\"={ \"name\":\"ice cream\", \"value\":1 } }", + .details.deposit.transaction_id = 1 }, + /* Try to double-spend the 5 EUR coin at the same merchant (but different + transaction ID) */ + { .oc = OC_PAY, + .label = "deposit-double-2", + .expected_response_code = MHD_HTTP_FORBIDDEN, + .details.deposit.amount = "EUR:5", + .details.deposit.coin_ref = "withdraw-coin-1", + .details.deposit.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.deposit.contract = "{ \"items\"={ \"name\":\"ice cream\", \"value\":1 } }", + .details.deposit.transaction_id = 2 }, + /* Try to double-spend the 5 EUR coin at the same merchant (but different + contract) */ + { .oc = OC_PAY, + .label = "deposit-double-3", + .expected_response_code = MHD_HTTP_FORBIDDEN, + .details.deposit.amount = "EUR:5", + .details.deposit.coin_ref = "withdraw-coin-1", + .details.deposit.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.deposit.contract = "{ \"items\"={ \"name\":\"ice cream\", \"value\":2 } }", + .details.deposit.transaction_id = 1 }, + + { .oc = OC_END } + }; + + is = GNUNET_new (struct InterpreterState); + is->commands = commands; + + ctx = TALER_MINT_init (); + GNUNET_assert (NULL != ctx); + ctx_task = GNUNET_SCHEDULER_add_now (&context_task, + ctx); + mint = TALER_MINT_connect (ctx, + "http://localhost:8081", + &cert_cb, is, + TALER_MINT_OPTION_END); + GNUNET_assert (NULL != mint); + 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 mint 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 *proc; + struct GNUNET_OS_Process *mintd; + struct GNUNET_OS_Process *merchantd; + + GNUNET_log_setup ("test-mint-api", + "WARNING", + NULL); + proc = GNUNET_OS_start_process (GNUNET_NO, + GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "taler-mint-keyup", + "taler-mint-keyup", + "-d", "test-mint-home", + "-m", "test-mint-home/master.priv", + NULL); + GNUNET_OS_process_wait (proc); + GNUNET_OS_process_destroy (proc); + mintd = GNUNET_OS_start_process (GNUNET_NO, + GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "taler-mint-httpd", + "taler-mint-httpd", + "-d", "test-mint-home", + NULL); + merchantd = GNUNET_OS_start_process (GNUNET_NO, + GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "taler-merchant-httpd", + "taler-merchant-httpd", + "-c", "test-merchant-home", + NULL); + /* give child time to start and bind against the socket */ + fprintf (stderr, "Waiting for taler-mint-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 (mintd, + SIGTERM); + GNUNET_OS_process_wait (mintd); + GNUNET_OS_process_destroy (mintd); + return (GNUNET_OK == result) ? 0 : 1; +} + +/* end of test_merchant_api.c */ |