merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit e3a8b30d8681cbc10f0357e74f8a09fa63f14c36
parent 5da68e54540be098f405ceef00fb648478bb73df
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 19 Jan 2016 14:39:23 +0100

Merge branch 'master' of git.taler.net:/var/git/merchant

Diffstat:
M.gitignore | 4++++
Mconfigure.ac | 57++++++++++++++++++++++++++++++++++++++++++++++++++-------
Dcontrib/merchant.conf | 15---------------
Am4/libcurl.m4 | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/merchant.conf | 5+++--
Msrc/backend/taler-merchant-httpd.c | 22+++++++++++++++++++---
Msrc/backend/taler-merchant-httpd.h | 20+++++++++++++++-----
Msrc/backend/taler-merchant-httpd_contract.c | 57++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/backend/taler-merchant-httpd_mints.c | 10++++++++++
Msrc/backend/taler-merchant-httpd_pay.c | 198++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/backenddb/plugin_merchantdb_postgres.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/frontend/checkout.php | 136+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Asrc/frontend/execute.js | 33+++++++++++++++++++++++++++++++++
Asrc/frontend/execute.php | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/frontend/execute.tsx | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/frontend/fulfillment.php | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/frontend/fullfillment.php | 50--------------------------------------------------
Msrc/frontend/generate_taler_contract.php | 34+++++++++++++++++++++-------------
Msrc/frontend/index.html | 324++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/frontend/pay.php | 85++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/frontend/style.css | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/Makefile.am | 3++-
Asrc/include/taler_merchant_service.h | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchantdb_plugin.h | 19++++++++++++++++---
Asrc/lib/Makefile.am | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/merchant_api_context.c | 536+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/merchant_api_context.h | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/merchant_api_json.c | 491+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/merchant_api_json.h | 331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/merchant_api_pay.c | 490+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/test-mint-home/config/mint-common.conf | 30++++++++++++++++++++++++++++++
Asrc/lib/test-mint-home/config/mint-keyup.conf | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/test-mint-home/master.priv | 2++
Asrc/lib/test-mint-home/sepa.json | 7+++++++
Asrc/lib/test_merchant.conf | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/test_merchant.priv | 2++
Asrc/lib/test_merchant_api.c | 1583+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tsconfig.json | 10++++++++++
38 files changed, 5442 insertions(+), 419 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -25,3 +25,7 @@ GPATH GRTAGS GTAGS *.swp +src/backend/taler-merchant-httpd +src/lib/test_merchant_api +taler_merchant_config.h +taler_merchant_config.h.in diff --git a/configure.ac b/configure.ac @@ -126,7 +126,8 @@ AS_IF([test $jansson = 0], *** ]])]) # check for libgnurl -LIBGNURL_CHECK_CONFIG([], [7.34.0], [gnurl=1], [gnurl=0]) +# libgnurl +LIBGNURL_CHECK_CONFIG(,7.34.0,gnurl=1,gnurl=0) if test "$gnurl" = 1 then AM_CONDITIONAL(HAVE_LIBGNURL, true) @@ -134,11 +135,52 @@ then else AM_CONDITIONAL(HAVE_LIBGNURL, false) fi -AS_IF([test $gnurl = 0], - [AC_MSG_ERROR([[ -*** -*** You need libgnurl to build this program. -*** ]])]) + +# libcurl-gnutls +LIBCURL_CHECK_CONFIG(,7.34.0,[curl=true],[curl=false]) +if test "x$curl" = xtrue +then + AC_CHECK_HEADERS([curl/curl.h], + AC_CHECK_DECLS(CURLINFO_TLS_SESSION,[curl=true],[curl=false],[[#include <curl/curl.h>]]), + [curl=false]) + # need libcurl-gnutls.so, everything else is not acceptable + AC_CHECK_LIB([curl-gnutls],[curl_easy_getinfo],,[curl=false]) + # cURL must support CURLINFO_TLS_SESSION, version >= 7.34 + +fi +if test x$curl = xfalse +then + AM_CONDITIONAL(HAVE_LIBCURL, false) +if test "$gnurl" = 0 +then + AC_MSG_WARN([GNUnet requires libcurl-gnutls >= 7.34]) +fi +else + AM_CONDITIONAL(HAVE_LIBCURL, true) + AC_DEFINE([HAVE_LIBCURL],[1],[Have CURL]) +fi + +# libgnurl +if test "x$gnurl" = "x0" +then + if test "x$curl" = "x0" + then + AC_MSG_NOTICE([NOTICE: libgnurl not found. http client support will not be compiled.]) + AC_MSG_WARN([ERROR: libgnurl not found. hostlist daemon will not be compiled, and you probably WANT the hostlist daemon]) + else + AC_MSG_NOTICE([WARNING: libgnurl not found, trying to use libcurl-gnutls instead.]) + fi +fi + +# gcov compilation +AC_MSG_CHECKING(whether to compile with support for code coverage analysis) +AC_ARG_ENABLE([coverage], + AS_HELP_STRING([--enable-coverage], + [compile the library with code coverage support]), + [use_gcov=${enableval}], + [use_gcov=no]) +AC_MSG_RESULT($use_gcov) +AM_CONDITIONAL([USE_COVERAGE], [test "x$use_gcov" = "xyes"]) # Require minimum libgcrypt version need_libgcrypt_version=1.6.1 @@ -173,5 +215,6 @@ AC_CONFIG_FILES([Makefile src/Makefile src/include/Makefile src/backenddb/Makefile -src/backend/Makefile]) +src/backend/Makefile +src/lib/Makefile]) AC_OUTPUT diff --git a/contrib/merchant.conf b/contrib/merchant.conf @@ -1,14 +0,0 @@ -[merchant] -PORT = 4251 - -# List of mints the merchant trusts delimited by a single space -TRUSTED_MINTS = taler - -[mint-taler] -HOSTNAME = taler.org -PORT = 4241 -# The public key of this mint -PUBKEY = ... - -[merchant-db] -CONFIG = postgres:///taler -\ No newline at end of file diff --git a/m4/libcurl.m4 b/m4/libcurl.m4 @@ -0,0 +1,251 @@ +# LIBCURL_CHECK_CONFIG ([DEFAULT-ACTION], [MINIMUM-VERSION], +# [ACTION-IF-YES], [ACTION-IF-NO]) +# ---------------------------------------------------------- +# David Shaw <dshaw@jabberwocky.com> May-09-2006 +# +# Checks for libcurl. DEFAULT-ACTION is the string yes or no to +# specify whether to default to --with-libcurl or --without-libcurl. +# If not supplied, DEFAULT-ACTION is yes. MINIMUM-VERSION is the +# minimum version of libcurl to accept. Pass the version as a regular +# version number like 7.10.1. If not supplied, any version is +# accepted. ACTION-IF-YES is a list of shell commands to run if +# libcurl was successfully found and passed the various tests. +# ACTION-IF-NO is a list of shell commands that are run otherwise. +# Note that using --without-libcurl does run ACTION-IF-NO. +# +# This macro #defines HAVE_LIBCURL if a working libcurl setup is +# found, and sets @LIBCURL@ and @LIBCURL_CPPFLAGS@ to the necessary +# values. Other useful defines are LIBCURL_FEATURE_xxx where xxx are +# the various features supported by libcurl, and LIBCURL_PROTOCOL_yyy +# where yyy are the various protocols supported by libcurl. Both xxx +# and yyy are capitalized. See the list of AH_TEMPLATEs at the top of +# the macro for the complete list of possible defines. Shell +# variables $libcurl_feature_xxx and $libcurl_protocol_yyy are also +# defined to 'yes' for those features and protocols that were found. +# Note that xxx and yyy keep the same capitalization as in the +# curl-config list (e.g. it's "HTTP" and not "http"). +# +# Users may override the detected values by doing something like: +# LIBCURL="-lcurl" LIBCURL_CPPFLAGS="-I/usr/myinclude" ./configure +# +# For the sake of sanity, this macro assumes that any libcurl that is +# found is after version 7.7.2, the first version that included the +# curl-config script. Note that it is very important for people +# packaging binary versions of libcurl to include this script! +# Without curl-config, we can only guess what protocols are available, +# or use curl_version_info to figure it out at runtime. + +AC_DEFUN([LIBCURL_CHECK_CONFIG], +[ + AH_TEMPLATE([LIBCURL_FEATURE_SSL],[Defined if libcurl supports SSL]) + AH_TEMPLATE([LIBCURL_FEATURE_KRB4],[Defined if libcurl supports KRB4]) + AH_TEMPLATE([LIBCURL_FEATURE_IPV6],[Defined if libcurl supports IPv6]) + AH_TEMPLATE([LIBCURL_FEATURE_LIBZ],[Defined if libcurl supports libz]) + AH_TEMPLATE([LIBCURL_FEATURE_ASYNCHDNS],[Defined if libcurl supports AsynchDNS]) + AH_TEMPLATE([LIBCURL_FEATURE_IDN],[Defined if libcurl supports IDN]) + AH_TEMPLATE([LIBCURL_FEATURE_SSPI],[Defined if libcurl supports SSPI]) + AH_TEMPLATE([LIBCURL_FEATURE_NTLM],[Defined if libcurl supports NTLM]) + + AH_TEMPLATE([LIBCURL_PROTOCOL_HTTP],[Defined if libcurl supports HTTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_HTTPS],[Defined if libcurl supports HTTPS]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FTP],[Defined if libcurl supports FTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FTPS],[Defined if libcurl supports FTPS]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FILE],[Defined if libcurl supports FILE]) + AH_TEMPLATE([LIBCURL_PROTOCOL_TELNET],[Defined if libcurl supports TELNET]) + AH_TEMPLATE([LIBCURL_PROTOCOL_LDAP],[Defined if libcurl supports LDAP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_DICT],[Defined if libcurl supports DICT]) + AH_TEMPLATE([LIBCURL_PROTOCOL_TFTP],[Defined if libcurl supports TFTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_RTSP],[Defined if libcurl supports RTSP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_POP3],[Defined if libcurl supports POP3]) + AH_TEMPLATE([LIBCURL_PROTOCOL_IMAP],[Defined if libcurl supports IMAP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_SMTP],[Defined if libcurl supports SMTP]) + + AC_ARG_WITH(libcurl, + AC_HELP_STRING([--with-libcurl=PREFIX],[look for the curl library in PREFIX/lib and headers in PREFIX/include]), + [_libcurl_with=$withval],[_libcurl_with=ifelse([$1],,[yes],[$1])]) + + if test "$_libcurl_with" != "no" ; then + + AC_PROG_AWK + + _libcurl_version_parse="eval $AWK '{split(\$NF,A,\".\"); X=256*256*A[[1]]+256*A[[2]]+A[[3]]; print X;}'" + + _libcurl_try_link=yes + + if test -d "$_libcurl_with" ; then + LIBCURL_CPPFLAGS="-I$withval/include" + _libcurl_ldflags="-L$withval/lib" + AC_PATH_PROG([_libcurl_config],[curl-config],[], + ["$withval/bin"]) + else + AC_PATH_PROG([_libcurl_config],[curl-config],[],[$PATH]) + fi + + if test x$_libcurl_config != "x" ; then + AC_CACHE_CHECK([for the version of libcurl], + [libcurl_cv_lib_curl_version], + [libcurl_cv_lib_curl_version=`$_libcurl_config --version | $AWK '{print $[]2}'`]) + + _libcurl_version=`echo $libcurl_cv_lib_curl_version | $_libcurl_version_parse` + _libcurl_wanted=`echo ifelse([$2],,[0],[$2]) | $_libcurl_version_parse` + + if test $_libcurl_wanted -gt 0 ; then + AC_CACHE_CHECK([for libcurl >= version $2], + [libcurl_cv_lib_version_ok], + [ + if test $_libcurl_version -ge $_libcurl_wanted ; then + libcurl_cv_lib_version_ok=yes + else + libcurl_cv_lib_version_ok=no + fi + ]) + fi + + if test $_libcurl_wanted -eq 0 || test x$libcurl_cv_lib_version_ok = xyes ; then + if test x"$LIBCURL_CPPFLAGS" = "x" ; then + LIBCURL_CPPFLAGS=`$_libcurl_config --cflags` + fi + if test x"$LIBCURL" = "x" ; then + LIBCURL=`$_libcurl_config --libs` + + # This is so silly, but Apple actually has a bug in their + # curl-config script. Fixed in Tiger, but there are still + # lots of Panther installs around. + case "${host}" in + powerpc-apple-darwin7*) + LIBCURL=`echo $LIBCURL | sed -e 's|-arch i386||g'` + ;; + esac + fi + + # All curl-config scripts support --feature + _libcurl_features=`$_libcurl_config --feature` + + # Is it modern enough to have --protocols? (7.12.4) + if test $_libcurl_version -ge 461828 ; then + _libcurl_protocols=`$_libcurl_config --protocols` + fi + else + _libcurl_try_link=no + fi + + unset _libcurl_wanted + fi + + if test $_libcurl_try_link = yes ; then + + # we didn't find curl-config, so let's see if the user-supplied + # link line (or failing that, "-lcurl") is enough. + LIBCURL=${LIBCURL-"$_libcurl_ldflags -lcurl"} + + AC_CACHE_CHECK([whether libcurl is usable], + [libcurl_cv_lib_curl_usable], + [ + _libcurl_save_cppflags=$CPPFLAGS + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + _libcurl_save_libs=$LIBS + LIBS="$LIBCURL $LIBS" + + AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <curl/curl.h>]],[[ +/* Try and use a few common options to force a failure if we are + missing symbols or can't link. */ +int x; +curl_easy_setopt(NULL,CURLOPT_URL,NULL); +x=CURL_ERROR_SIZE; +x=CURLOPT_WRITEFUNCTION; +x=CURLOPT_WRITEDATA; +x=CURLOPT_ERRORBUFFER; +x=CURLOPT_STDERR; +x=CURLOPT_VERBOSE; +if (x) ; +]])],libcurl_cv_lib_curl_usable=yes,libcurl_cv_lib_curl_usable=no) + + CPPFLAGS=$_libcurl_save_cppflags + LIBS=$_libcurl_save_libs + unset _libcurl_save_cppflags + unset _libcurl_save_libs + ]) + + if test $libcurl_cv_lib_curl_usable = yes ; then + + # Does curl_free() exist in this version of libcurl? + # If not, fake it with free() + + _libcurl_save_cppflags=$CPPFLAGS + CPPFLAGS="$CPPFLAGS $LIBCURL_CPPFLAGS" + _libcurl_save_libs=$LIBS + LIBS="$LIBS $LIBCURL" + + AC_CHECK_FUNC(curl_free,, + AC_DEFINE(curl_free,free, + [Define curl_free() as free() if our version of curl lacks curl_free.])) + + CPPFLAGS=$_libcurl_save_cppflags + LIBS=$_libcurl_save_libs + unset _libcurl_save_cppflags + unset _libcurl_save_libs + + AC_DEFINE(HAVE_LIBCURL,1, + [Define to 1 if you have a functional curl library.]) + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL) + + for _libcurl_feature in $_libcurl_features ; do + AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_feature_$_libcurl_feature),[1]) + eval AS_TR_SH(libcurl_feature_$_libcurl_feature)=yes + done + + if test "x$_libcurl_protocols" = "x" ; then + + # We don't have --protocols, so just assume that all + # protocols are available + _libcurl_protocols="HTTP FTP FILE TELNET LDAP DICT TFTP" + + if test x$libcurl_feature_SSL = xyes ; then + _libcurl_protocols="$_libcurl_protocols HTTPS" + + # FTPS wasn't standards-compliant until version + # 7.11.0 (0x070b00 == 461568) + if test $_libcurl_version -ge 461568; then + _libcurl_protocols="$_libcurl_protocols FTPS" + fi + fi + + # RTSP, IMAP, POP3 and SMTP were added in + # 7.20.0 (0x071400 == 463872) + if test $_libcurl_version -ge 463872; then + _libcurl_protocols="$_libcurl_protocols RTSP IMAP POP3 SMTP" + fi + fi + + for _libcurl_protocol in $_libcurl_protocols ; do + AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_protocol_$_libcurl_protocol),[1]) + eval AS_TR_SH(libcurl_protocol_$_libcurl_protocol)=yes + done + else + unset LIBCURL + unset LIBCURL_CPPFLAGS + fi + fi + + unset _libcurl_try_link + unset _libcurl_version_parse + unset _libcurl_config + unset _libcurl_feature + unset _libcurl_features + unset _libcurl_protocol + unset _libcurl_protocols + unset _libcurl_version + unset _libcurl_ldflags + fi + + if test x$_libcurl_with = xno || test x$libcurl_cv_lib_curl_usable != xyes ; then + # This is the IF-NO path + ifelse([$4],,:,[$4]) + else + # This is the IF-YES path + ifelse([$3],,:,[$3]) + fi + + unset _libcurl_with +])dnl diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -25,7 +25,7 @@ EDATE = 3 week DB = postgres [mint-taler] -URI = mint.demo.taler.net +URI = http://mint.demo.taler.net/ MASTER_KEY = Q1WVGRGC1F4W7RYC6M23AEGFEXQEHQ730K3GG0B67VPHQSRR75H0 # Auditors must be in sections "auditor-", the rest of the section @@ -46,9 +46,10 @@ PUBLIC_KEY = 9QXF7XY7E9VPV47B5Z806NDFSX2VJ79SVHHD29QEQ3BG31ANHZ60 [merchantdb-postgres] CONFIG = postgres:///talerdemo - # "wire-" sections include wire details, here for SEPA. [wire-sepa] IBAN = DE67830654080004822650 NAME = GNUNET E.V BIC = GENODEF1SRL +ADDRESS = "Garching" +SALT = 12345 diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -334,6 +334,7 @@ parse_wireformat_sepa (const struct GNUNET_CONFIGURATION_Handle *cfg) unsigned long long salt; char *iban; char *name; + char *address; char *bic; if (GNUNET_OK != @@ -366,21 +367,35 @@ parse_wireformat_sepa (const struct GNUNET_CONFIGURATION_Handle *cfg) if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "wire-sepa", + "ADDRESS", + &address)) + { + GNUNET_free (iban); + GNUNET_free (name); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "wire-sepa", "BIC", &bic)) { GNUNET_free (iban); GNUNET_free (name); GNUNET_free (bic); + GNUNET_free (address); + return GNUNET_SYSERR; } - j_wire = json_pack ("{s:s, s:s, s:s, s:s, s:o}", + j_wire = json_pack ("{s:s, s:s, s:s, s:s, s:o, s:s}", "type", "SEPA", "IBAN", iban, "name", name, "bic", bic, - "r", json_integer (salt)); + "r", json_integer (salt), + "address", address); GNUNET_free (iban); GNUNET_free (name); + GNUNET_free (address); GNUNET_free (bic); if (NULL == j_wire) return GNUNET_SYSERR; @@ -454,6 +469,7 @@ prepare_daemon () tv = GNUNET_TIME_UNIT_FOREVER_REL; GNUNET_NETWORK_fdset_copy_native (wrs, &rs, max + 1); GNUNET_NETWORK_fdset_copy_native (wws, &ws, max + 1); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Adding run_daemon select task\n"); ret = GNUNET_SCHEDULER_add_select (GNUNET_SCHEDULER_PRIORITY_HIGH, tv, wrs, wws, @@ -491,7 +507,7 @@ run (void *cls, TMH_AUDITORS_init (config)); /* FIXME: for now, we just support SEPA here: */ EXITIF (GNUNET_OK != - parse_wireformat_sepa (config)); + parse_wireformat_sepa (config)); EXITIF (GNUNET_OK != validate_and_hash_wireformat ("SEPA")); EXITIF (GNUNET_OK != diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -136,19 +136,29 @@ extern json_t *j_wire; */ extern struct GNUNET_HashCode h_wire; - +/** + * Our private key (for the merchant to sign contracts). + */ extern struct GNUNET_CRYPTO_EddsaPrivateKey *privkey; +/** + * Our public key, corresponds to #privkey. + */ extern struct TALER_MerchantPublicKeyP pubkey; - +/** + * Handle to the database backend. + */ extern struct TALER_MERCHANTDB_Plugin *db; - - +/** + * If the frontend does NOT specify an execution date, how long should + * we tell the mint to wait to aggregate transactions before + * executing? This delay is added to the current time when we + * generate the advisory execution time for the mint. + */ extern struct GNUNET_TIME_Relative edate_delay; - /** * Kick MHD to run now, to be called after MHD_resume_connection(). * Basically, we need to explicitly resume MHD's event loop whenever diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c @@ -52,8 +52,10 @@ MH_handler_contract (struct TMH_RequestHandler *rh, size_t *upload_data_size) { json_t *root; + json_t *jcontract; + json_t *pay_url; + json_t *exec_url; int res; - struct GNUNET_HashCode h_wire; struct TALER_ContractPS contract; struct GNUNET_CRYPTO_EddsaSignature contract_sig; @@ -68,39 +70,64 @@ MH_handler_contract (struct TMH_RequestHandler *rh, if ((GNUNET_NO == res) || (NULL == root)) return MHD_YES; - /* add fields to the "root" that the backend should provide */ - json_object_set (root, + jcontract = json_object_get (root, "contract"); + + if (NULL == jcontract) + { + return TMH_RESPONSE_reply_internal_error (connection, + "contract request malformed"); + } + + /* add fields to the contract that the backend should provide */ + json_object_set (jcontract, "mints", trusted_mints); - json_object_set (root, + json_object_set (jcontract, "auditors", j_auditors); - json_object_set_new (root, + json_object_set_new (jcontract, "H_wire", TALER_json_from_data (&h_wire, sizeof (h_wire))); - json_object_set_new (root, + json_object_set_new (jcontract, "merchant_pub", TALER_json_from_data (&pubkey, sizeof (pubkey))); /* create contract signature */ GNUNET_assert (GNUNET_OK == - TALER_hash_json (root, + TALER_hash_json (jcontract, &contract.h_contract)); contract.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT); contract.purpose.size = htonl (sizeof (contract)); GNUNET_CRYPTO_eddsa_sign (privkey, &contract.purpose, &contract_sig); + + pay_url = json_object_get (root, "pay_url"); + if (NULL == pay_url) + { + return TMH_RESPONSE_reply_internal_error (connection, + "pay url missing"); + } + exec_url = json_object_get (root, "exec_url"); + if (NULL == exec_url) + { + return TMH_RESPONSE_reply_internal_error (connection, + "exec url missing"); + } /* return final response */ - return TMH_RESPONSE_reply_json_pack (connection, - MHD_HTTP_OK, - "{s:o, s:o, s:o}", - "contract", root, - "sig", TALER_json_from_data (&contract_sig, - sizeof (contract_sig)), - "H_contract", TALER_json_from_data (&contract.h_contract, - sizeof (contract.h_contract))); + res = TMH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:O, s:O, s:O, s:o, s:o}", + "contract", jcontract, + "exec_url", exec_url, + "pay_url", pay_url, + "sig", TALER_json_from_data (&contract_sig, + sizeof (contract_sig)), + "H_contract", TALER_json_from_data (&contract.h_contract, + sizeof (contract.h_contract))); + json_decref (root); + return res; } /* end of taler-merchant-httpd_contract.c */ diff --git a/src/backend/taler-merchant-httpd_mints.c b/src/backend/taler-merchant-httpd_mints.c @@ -230,6 +230,8 @@ context_task (void *cls, struct GNUNET_NETWORK_FDSet *ws; struct GNUNET_TIME_Relative delay; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "In mint context polling task\n"); + poller_task = NULL; TALER_MINT_perform (ctx); max_fd = -1; @@ -243,6 +245,9 @@ context_task (void *cls, &except_fd_set, &max_fd, &timeout); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "In mint context polling task, max_fd=%d, timeout=%ld\n", + max_fd, timeout); if (timeout >= 0) delay = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, @@ -319,6 +324,11 @@ TMH_MINTS_find_mint (const char *chosen_mint, GNUNET_break (0); return NULL; } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Trying to find chosen mint `%s'\n", + chosen_mint); + /* Check if the mint is known */ for (mint = mint_head; NULL != mint; mint = mint->next) /* test it by checking public key --- FIXME: hostname or public key!? diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c @@ -15,8 +15,9 @@ */ /** * @file backend/taler-merchant-httpd_pay.c - * @brief HTTP serving layer mainly intended to communicate with the frontend + * @brief handling of /pay requests * @author Marcello Stanisci + * @author Christian Grothoff */ #include "platform.h" #include <jansson.h> @@ -67,6 +68,11 @@ struct MERCHANT_DepositConfirmation struct TALER_Amount percoin_amount; /** + * Amount this coin contributes to the total purchase price. + */ + struct TALER_Amount amount_without_fee; + + /** * Public key of the coin. */ struct TALER_CoinSpendPublicKeyP coin_pub; @@ -112,6 +118,12 @@ struct PayContext struct MHD_Connection *connection; /** + * Handle to the mint that we are doing the payment with. + * (initially NULL while @e fo is trying to find a mint). + */ + struct TALER_MINT_Handle *mh; + + /** * Handle for operation to lookup /keys (and auditors) from * the mint used for this transaction; NULL if no operation is * pending. @@ -219,6 +231,29 @@ resume_pay_with_response (struct PayContext *pc, /** + * Abort all pending /deposit operations. + * + * @param pc pay context to abort + */ +static void +abort_deposit (struct PayContext *pc) +{ + unsigned int i; + + for (i=0;i<pc->coins_cnt;i++) + { + struct MERCHANT_DepositConfirmation *dci = &pc->dc[i]; + + if (NULL != dci->dh) + { + TALER_MINT_deposit_cancel (dci->dh); + dci->dh = NULL; + } + } +} + + +/** * Callback to handle a deposit permission's response. * * @param cls a `struct MERCHANT_DepositConfirmation` (i.e. a pointer @@ -245,26 +280,38 @@ deposit_cb (void *cls, pc->pending--; if (MHD_HTTP_OK != http_status) { - unsigned int i; - /* Transaction failed; stop all other ongoing deposits */ - for (i=0;i<pc->coins_cnt;i++) - { - struct MERCHANT_DepositConfirmation *dci = &pc->dc[i]; - - if (NULL != dci->dh) - { - TALER_MINT_deposit_cancel (dci->dh); - dci->dh = NULL; - } - } + abort_deposit (pc); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Deposit operation failed with HTTP code %u\n", + http_status); /* Forward error including 'proof' for the body */ resume_pay_with_response (pc, http_status, TMH_RESPONSE_make_json (proof)); return; } - /* FIXME: store result to DB here somewhere! */ + /* store result to DB */ + if (GNUNET_OK != + db->store_payment (db->cls, + &pc->h_contract, + &h_wire, + pc->transaction_id, + pc->timestamp, + pc->refund_deadline, + &dc->amount_without_fee, + &dc->coin_pub, + proof)) + { + GNUNET_break (0); + /* internal error */ + abort_deposit (pc); + /* Forward error including 'proof' for the body */ + resume_pay_with_response (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TMH_RESPONSE_make_internal_error ("Merchant database error")); + return; + } if (0 != pc->pending) return; /* still more to do */ resume_pay_with_response (pc, @@ -351,6 +398,7 @@ process_pay_with_mint (void *cls, TMH_RESPONSE_make_external_error ("mint not supported")); return; } + pc->mh = mh; keys = TALER_MINT_get_keys (mh); if (NULL == keys) @@ -372,6 +420,7 @@ process_pay_with_mint (void *cls, &dc->denom); if (NULL == denom_details) { + GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_json_pack ("{s:s, s:o}", @@ -384,6 +433,7 @@ process_pay_with_mint (void *cls, denom_details, mint_trusted)) { + GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_BAD_REQUEST, TMH_RESPONSE_make_json_pack ("{s:s, s:o}", @@ -398,12 +448,37 @@ process_pay_with_mint (void *cls, } else { - TALER_amount_add (&acc_fee, - &denom_details->fee_deposit, - &acc_fee); - TALER_amount_add (&acc_amount, - &dc->percoin_amount, - &acc_amount); + if ( (GNUNET_OK != + TALER_amount_add (&acc_fee, + &denom_details->fee_deposit, + &acc_fee)) || + (GNUNET_OK != + TALER_amount_add (&acc_amount, + &dc->percoin_amount, + &acc_amount)) ) + { + GNUNET_break_op (0); + /* Overflow in these amounts? Very strange. */ + resume_pay_with_response (pc, + MHD_HTTP_BAD_REQUEST, + TMH_RESPONSE_make_internal_error ("Overflow adding up amounts")); + return; + } + } + if (GNUNET_SYSERR == + TALER_amount_subtract (&dc->amount_without_fee, + &dc->percoin_amount, + &denom_details->fee_deposit)) + { + GNUNET_break_op (0); + /* fee higher than residual coin value, makes no sense. */ + resume_pay_with_response (pc, + MHD_HTTP_BAD_REQUEST, + TMH_RESPONSE_make_json_pack ("{s:s, s:o, s:o}", + "hint", "fee higher than coin value", + "f", TALER_json_from_amount (&dc->percoin_amount), + "fee_deposit", TALER_json_from_amount (&denom_details->fee_deposit))); + return; } } @@ -436,6 +511,7 @@ process_pay_with_mint (void *cls, if (-1 == TALER_amount_cmp (&acc_amount, &total_needed)) { + GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_NOT_ACCEPTABLE, TMH_RESPONSE_make_external_error ("insufficient funds (including excessive mint fees to be covered by customer)")); @@ -448,6 +524,7 @@ process_pay_with_mint (void *cls, if (-1 == TALER_amount_cmp (&acc_amount, &pc->amount)) { + GNUNET_break_op (0); resume_pay_with_response (pc, MHD_HTTP_NOT_ACCEPTABLE, TMH_RESPONSE_make_external_error ("insufficient funds")); @@ -477,11 +554,14 @@ process_pay_with_mint (void *cls, dc); if (NULL == dc->dh) { + /* Signature was invalid. If the mint was unavailable, + * we'd get that information in the callback. */ + GNUNET_break_op (0); resume_pay_with_response (pc, - MHD_HTTP_SERVICE_UNAVAILABLE, + MHD_HTTP_UNAUTHORIZED, TMH_RESPONSE_make_json_pack ("{s:s, s:i}", - "mint", pc->chosen_mint, - "transaction_id", pc->transaction_id)); + "hint", "Coin signature invalid.", + "coin_idx", i)); return; } } @@ -511,6 +591,8 @@ MH_handler_pay (struct TMH_RequestHandler *rh, int res; json_t *root; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "In handler for /pay.\n"); + if (NULL == *connection_cls) { pc = GNUNET_new (struct PayContext); @@ -528,6 +610,7 @@ MH_handler_pay (struct TMH_RequestHandler *rh, /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == pc->response_code) return MHD_NO; /* hard error */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Queueing response for /pay.\n"); res = MHD_queue_response (connection, pc->response_code, pc->response); @@ -549,23 +632,6 @@ MH_handler_pay (struct TMH_RequestHandler *rh, if ((GNUNET_NO == res) || (NULL == root)) return MHD_YES; /* the POST's body has to be further fetched */ - /* We got no 'edate' from frontend. Generate it here; it will be - timestamp plus the edate_delay supplied in config file */ - if (NULL == json_object_get (root, "edate")) - { - pc->edate = GNUNET_TIME_absolute_add (pc->timestamp, // FIXME: uninit! - edate_delay); - if (-1 == - json_object_set (root, - "edate", - TALER_json_from_abs (pc->edate))) - { - GNUNET_break (0); - json_decref (root); - return MHD_NO; - } - } - /* Got the JSON upload, parse it */ { json_t *coins; @@ -580,19 +646,47 @@ MH_handler_pay (struct TMH_RequestHandler *rh, TMH_PARSE_member_time_abs ("timestamp", &pc->timestamp), TMH_PARSE_member_time_abs ("refund_deadline", &pc->refund_deadline), TMH_PARSE_member_fixed ("H_contract", &pc->h_contract), - TMH_PARSE_member_time_abs ("edate", &pc->edate), TMH_PARSE_MEMBER_END }; res = TMH_PARSE_json_data (connection, root, spec); + if (GNUNET_YES != res) { json_decref (root); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Parsed JSON for /pay.\n"); + + /* 'edate' is optional, if it is not present, generate it here; it + will be timestamp plus the edate_delay supplied in config + file */ + if (NULL == json_object_get (root, "edate")) + { + pc->edate = GNUNET_TIME_absolute_add (pc->timestamp, + edate_delay); + } + else + { + struct TMH_PARSE_FieldSpecification espec[] = { + TMH_PARSE_member_time_abs ("edate", &pc->edate), + TMH_PARSE_MEMBER_END + }; + + res = TMH_PARSE_json_data (connection, + root, + espec); + if (GNUNET_YES != res) + { + json_decref (root); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + + pc->coins_cnt = json_array_size (coins); if (0 == pc->coins_cnt) { @@ -600,9 +694,15 @@ MH_handler_pay (struct TMH_RequestHandler *rh, return TMH_RESPONSE_reply_external_error (connection, "no coins given"); } + /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct MERCHANT_DepositConfirmation); + { + char *s = json_dumps (coins, JSON_INDENT(2)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Coins json is: %s\n", s); + } + json_array_foreach (coins, coins_index, coin) { struct MERCHANT_DepositConfirmation *dc = &pc->dc[coins_index]; @@ -623,10 +723,26 @@ MH_handler_pay (struct TMH_RequestHandler *rh, json_decref (root); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } + + { + char *s; + s = TALER_amount_to_string (&dc->percoin_amount); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin #%i has f %s\n", coins_index, s); + GNUNET_free (s); + } + dc->index = coins_index; + dc->pc = pc; } } + /* Check if this payment attempt has already taken place */ + if (GNUNET_OK == db->check_payment (db->cls, pc->transaction_id)) + return TMH_RESPONSE_reply_external_error (connection, "payment already attempted"); + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Looking up chosen mint '%s'\n", pc->chosen_mint); + /* Find the responsible mint, this may take a while... */ pc->pending = pc->coins_cnt; pc->fo = TMH_MINTS_find_mint (pc->chosen_mint, diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -70,14 +70,14 @@ postgres_initialize (void *cls, "CREATE %1$s TABLE IF NOT EXISTS payments (" "h_contract BYTEA NOT NULL," "h_wire BYTEA NOT NULL," - "transaction_id INT8 PRIMARY KEY," + "transaction_id INT8," /*WARNING: this column used to be primary key, but that wrong since multiple coins belong to the same id*/ "timestamp INT8 NOT NULL," "refund_deadline INT8 NOT NULL," "amount_without_fee_val INT8 NOT NULL," "amount_without_fee_frac INT4 NOT NULL," "amount_without_fee_curr VARCHAR(" TALER_CURRENCY_LEN_STR ") NOT NULL," "coin_pub BYTEA NOT NULL," - "mint_sig BYTEA NOT NULL);", + "mint_proof BYTEA NOT NULL);", tmp_str); ret = GNUNET_POSTGRES_exec (pg->conn, sql); @@ -96,7 +96,7 @@ postgres_initialize (void *cls, ",amount_without_fee_frac" ",amount_without_fee_curr" ",coin_pub" - ",mint_sig) VALUES " + ",mint_proof) VALUES " "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", 10, NULL))) || (PGRES_COMMAND_OK != (status = PQresultStatus(res))) ) @@ -108,6 +108,22 @@ postgres_initialize (void *cls, } return GNUNET_SYSERR; } + if ( (NULL == (res = PQprepare (pg->conn, + "check_payment", + "SELECT * " + "FROM payments " + "WHERE transaction_id=$1", + 1, NULL))) || + (PGRES_COMMAND_OK != (status = PQresultStatus(res))) ) + { + if (NULL != res) + { + PQSQL_strerror (GNUNET_ERROR_TYPE_ERROR, "PQprepare", res); + PQclear (res); + } + return GNUNET_SYSERR; + } + PQclear (res); return GNUNET_OK; } @@ -124,7 +140,7 @@ postgres_initialize (void *cls, * @param refund refund deadline * @param amount_without_fee amount the mint will deposit * @param coin_pub public key of the coin - * @param merchant_pub our public key + * @param mint_proof proof from the mint that coin was accepted * @return #GNUNET_OK on success, #GNUNET_SYSERR upon error */ static int @@ -136,7 +152,7 @@ postgres_store_payment (void *cls, struct GNUNET_TIME_Absolute refund, const struct TALER_Amount *amount_without_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_MintSignatureP *mint_sig) + json_t *mint_proof) { struct PostgresClosure *pg = cls; PGresult *res; @@ -150,7 +166,7 @@ postgres_store_payment (void *cls, TALER_PQ_query_param_absolute_time (&refund), TALER_PQ_query_param_amount (amount_without_fee), TALER_PQ_query_param_auto_from_type (coin_pub), - TALER_PQ_query_param_auto_from_type (mint_sig), + TALER_PQ_query_param_json (mint_proof), TALER_PQ_query_param_end }; @@ -190,7 +206,61 @@ postgres_store_payment (void *cls, return GNUNET_OK; } +/** + * Check whether a payment has already been stored + * + * @param cls our plugin handle + * @param transaction_id the transaction id to search into + * the db + * + * @return GNUNET_OK if found, GNUNET_NO if not, GNUNET_SYSERR + * upon error + */ +static int +postgres_check_payment(void *cls, + uint64_t transaction_id) +{ + struct PostgresClosure *pg = cls; + PGresult *res; + ExecStatusType status; + struct TALER_PQ_QueryParam params[] = { + TALER_PQ_query_param_uint64 (&transaction_id), + TALER_PQ_query_param_end + }; + res = TALER_PQ_exec_prepared (pg->conn, + "check_payment", + params); + + status = PQresultStatus (res); + if (PGRES_TUPLES_OK != status) + { + const char *sqlstate; + + sqlstate = PQresultErrorField (res, PG_DIAG_SQLSTATE); + if (NULL == sqlstate) + { + /* very unexpected... */ + GNUNET_break (0); + PQclear (res); + return GNUNET_SYSERR; + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not check if contract %llu is in DB: %s\n", + transaction_id, + sqlstate); + PQclear (res); + return GNUNET_SYSERR; + } + /* count rows */ + if (PQntuples (res) > 0) + return GNUNET_OK; + return GNUNET_NO; + + + +} /** * Initialize Postgres database subsystem. * @@ -220,6 +290,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->cls = pg; plugin->initialize = &postgres_initialize; plugin->store_payment = &postgres_store_payment; + plugin->check_payment = &postgres_check_payment; return plugin; } diff --git a/src/frontend/checkout.php b/src/frontend/checkout.php @@ -1,7 +1,8 @@ <!DOCTYPE html> <html> <head> -<title>Taler's "Demo" Shop: Choose payment method</title> + <title>Toy Store - Payment method - Taler Demo</title> + <link rel="stylesheet" type="text/css" href="style.css"> <script> /* @licstart The following is the entire license notice for the @@ -31,7 +32,7 @@ </head> <body onload="signal_taler_wallet_onload()"> <!-- - This page's main aim is to show to the customer all the accepted + This main goal of this page is to show to the customer all the accepted payments methods and actually implementing just Taler; technically the steps are: @@ -50,52 +51,78 @@ $donation_currency = $_POST['donation_currency']; // get frational part - list ($donation_value, $donation_fraction) = split ("\.", $donation_amount, 2); + list ($donation_value, $donation_fraction) = explode (".", $donation_amount, 2); // create PHP session and store donation information in session $donation_fraction = (float) ("0." . $donation_fraction); session_start(); + session_unset(); $_SESSION['receiver'] = $donation_receiver; $_SESSION['amount_value'] = (int) $donation_amount; $_SESSION['amount_fraction'] = (int) ($donation_fraction * 1000000); $_SESSION['currency'] = $donation_currency; ?> -<h2>Select your payment option</h2> -<div> -This is an example for a "checkout" page of a Web shop. -On the previous page, you have created the shopping cart -and decided which product to buy (i.e. which project to -donate KUDOS to). Now in this page, you are asked to -select a payment option. As Taler is not yet universally -used, we expect merchants will offer various payment options. -<p> -The page also demonstrates how to only enable (or show) the Taler -option if Taler is actually supported by the browser. For example, -if you disable the Taler extension now, the Taler payment option -will be disabled in the page. Naturally, you could also trivially -hide the Taler option entirely by changing the visibility instead. -<p> -Note that you MUST select Taler here for the demo to continue, -as the other payment options are just placeholders and not -really working in the demonstration. Also, it is of course -possible to ask the user to make this choice already on the -previous page (with the shopping cart), we just separated the -two steps to keep each step as simple as possible. -</div> -<form name="tform" action="" method="POST"> - <div id="opt-form" align="left"><br> - <input type="radio" name="payment_system" value="lisa" - id="lisa-radio-button-id">Lisa</input> - <br> - <input type="radio" name="payment_system" value="ycard">You Card</input> - <br> - <input type="radio" name="payment_system" value="cardme">Card Me</input> - <br> - <input type="radio" name="payment_system" value="taler" - id="taler-radio-button-id" disabled="true">Taler</input> - <br> - <input type="button" onclick="pay(this.form)" value="Ok"> - </div> -</form> + + <header> + <div id="logo"> + <svg height="100" width="100"> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> + </svg> + </div> + + <h1>Toy Store - Select payment method</h1> + </header> + + <aside class="sidebar" id="left"> + </aside> + + <section id="main"> + <article> + + <h1>Select your payment method</h1> + + <p> + This is an example for a "checkout" page of a Web shop. + On the previous page, you have created the shopping cart + and decided which product to buy (i.e. which project to + donate KUDOS to). Now in this page, you are asked to + select a payment option. As Taler is not yet universally + used, we expect merchants will offer various payment options. + </p> + <p> + The page also demonstrates how to only enable (or show) the Taler + option if Taler is actually supported by the browser. For example, + if you disable the Taler extension now, the Taler payment option + will be disabled in the page. Naturally, you could also trivially + hide the Taler option entirely by changing the visibility instead. + </p> + <p> + Note that you MUST select Taler here for the demo to continue, + as the other payment options are just placeholders and not + really working in the demonstration. Also, it is of course + possible to ask the user to make this choice already on the + previous page (with the shopping cart), we just separated the + two steps to keep each step as simple as possible. + </p> + + <form name="tform" action="" method="POST"> + <div id="opt-form" align="left"><br> + <input type="radio" name="payment_system" value="lisa" + id="lisa-radio-button-id">Lisa</input> + <br/> + <input type="radio" name="payment_system" value="ycard">You Card</input> + <br/> + <input type="radio" name="payment_system" value="cardme">Card Me</input> + <br/> + <input type="radio" name="payment_system" value="taler" + id="taler-radio-button-id" disabled="true">Taler</input> + <br/> + <input type="button" onclick="pay(this.form)" value="Ok"></input> + </div> + </form> + + </article> + </section> <script type="text/javascript"> @@ -106,7 +133,7 @@ function handle_contract(json_contract) { var cEvent = new CustomEvent('taler-contract', { detail: json_contract }); - document.body.dispatchEvent(cEvent); + document.dispatchEvent(cEvent); }; @@ -121,7 +148,7 @@ function taler_pay(form) have its own way of generating and transmitting the contract, there just must be a way to get the contract and to pass it to the wallet when the user selects 'Pay'. */ - contract_request.open("GET", "/generate_taler_contract.php", true); + contract_request.open("GET", "generate_taler_contract.php", true); contract_request.onload = function (e) { if (contract_request.readyState == 4) @@ -129,9 +156,9 @@ function taler_pay(form) if (contract_request.status == 200) { /* display contract_requestificate (i.e. it sends the JSON string - to the extension) alert (contract_request.responseText); */ + to the extension) alert (contract_request.responseText); */ + console.log("response text:", contract_request.responseText); handle_contract(contract_request.responseText); - } else { @@ -211,7 +238,7 @@ function taler_wallet_unload_cb(aEvent) function signal_taler_wallet_onload() { var eve = new Event('taler-checkout-probe'); - document.body.dispatchEvent(eve); + document.dispatchEvent(eve); }; @@ -228,21 +255,20 @@ function test_without_wallet(){ // Register event to be triggered by the wallet as a response to our // first event -document.body.addEventListener("taler-wallet-present", - has_taler_wallet_cb, - false); +document.addEventListener("taler-wallet-present", + has_taler_wallet_cb, + false); // Register event to be triggered by the wallet when it gets enabled while // the user is on the payment page -document.body.addEventListener("taler-load", - signal_taler_wallet_onload, - false); +document.addEventListener("taler-load", + signal_taler_wallet_onload, + false); // Register event to be triggered by the wallet when it is unloaded -document.body.addEventListener("taler-unload", - taler_wallet_unload_cb, - false); - +document.addEventListener("taler-unload", + taler_wallet_unload_cb, + false); </script> </body> </html> diff --git a/src/frontend/execute.js b/src/frontend/execute.js @@ -0,0 +1,33 @@ +"use strict"; +// JSX literals are compiled to calls to React.createElement calls. +let React = { + createElement: function (tag, props, ...children) { + let e = document.createElement(tag); + for (let k in props) { + e.setAttribute(k, props[k]); + } + for (let child of children) { + if ("string" === typeof child || "number" == typeof child) { + child = document.createTextNode(child); + } + e.appendChild(child); + } + return e; + } +}; +document.addEventListener("DOMContentLoaded", function (e) { + var eve = new CustomEvent('taler-execute-payment', { detail: { H_contract: h_contract } }); + document.dispatchEvent(eve); +}); +function replace(el, r) { + el.parentNode.replaceChild(r, el); +} +document.addEventListener("taler-payment-result", function (e) { + if (!e.detail.success) { + alert("Payment failed\n" + JSON.stringify(e.detail)); + return; + } + console.log("finished payment"); + let msg = React.createElement("div", null, "Payment successful. View your ", React.createElement("a", {"href": e.detail.fulfillmentUrl}, "product"), "."); + replace(document.getElementById("loading"), msg); +}); diff --git a/src/frontend/execute.php b/src/frontend/execute.php @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Toy Store - Taler Demo</title> + <link rel="stylesheet" type="text/css" href="style.css"> + <script> /* @licstart The following is the entire license notice for the + JavaScript code in this page. + + Copyright (C) 2015 GNUnet e.V. + + The JavaScript code in this page is free software: you can + redistribute it and/or modify it under the terms of the GNU + Lesser General Public License (GNU LGPL) as published by the Free Software + Foundation, either version 2.1 of the License, or (at your option) + any later version. The code is distributed WITHOUT ANY WARRANTY; + without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU LGPL for more details. + + As additional permission under GNU LGPL version 2.1 section 7, you + may distribute non-source (e.g., minimized or compacted) forms of + that code without the copy of the GNU LGPL normally required by + section 4, provided you include this license notice and a URL + through which recipients can access the Corresponding Source. + + @licend The above is the entire license notice + for the JavaScript code in this page. + */ + </script> + <script type="text/javascript"> +<?php +session_start(); +echo "var h_contract=\"$_SESSION[H_contract]\";\n"; +?> + </script> + <script type="text/javascript" src="execute.js"></script> +</head> + +<body> + <header> + <div id="logo"> + <svg height="100" width="100"> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> + </svg> + </div> + <h1>Toy Store - Taler Demo</h1> + </header> + + <aside class="sidebar" id="left"> + </aside> + + <section id="main"> + <h1>Executing Payment ...</h1> + <div id="loading">Loading...</div> +</body> +</html> diff --git a/src/frontend/execute.tsx b/src/frontend/execute.tsx @@ -0,0 +1,41 @@ +"use strict"; + + +// JSX literals are compiled to calls to React.createElement calls. +let React = { + createElement: function(tag, props, ...children) { + let e = document.createElement(tag); + for (let k in props) { + e.setAttribute(k, props[k]); + } + for (let child of children) { + if ("string" === typeof child || "number" == typeof child) { + child = document.createTextNode(child); + } + e.appendChild(child); + } + return e; + } +}; + +declare var h_contract: string; + +document.addEventListener("DOMContentLoaded", function (e) { + var eve = new CustomEvent('taler-execute-payment', {detail: {H_contract: h_contract}}); + document.dispatchEvent(eve); +}); + +function replace(el, r) { + el.parentNode.replaceChild(r, el); +} + +document.addEventListener("taler-payment-result", function (e: CustomEvent) { + if (!e.detail.success) { + alert("Payment failed\n" + JSON.stringify(e.detail)); + return; + } + console.log("finished payment"); + let msg = + <div>Payment successful. View your <a href={e.detail.fulfillmentUrl}>product</a>.</div>; + replace(document.getElementById("loading"), msg); +}); diff --git a/src/frontend/fulfillment.php b/src/frontend/fulfillment.php @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Taler's "Demo" Shop</title> + <link rel="stylesheet" type="text/css" href="style.css"> +</head> +<body> + + <header> + <div id="logo"> + <svg height="100" width="100"> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> + </svg> + </div> + + <h1>Toy Store - Payment succeeded</h1> + </header> + + <aside class="sidebar" id="left"> + </aside> + + <section id="main"> + <article> +<?php +/* + This file is part of GNU 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 Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> + +*/ + +$cli_debug = false; +$backend_test = true; + +function generate_msg ($link){ + $msg = "<p>Thanks for donating to " . $_SESSION['receiver'] . ".</p>"; + if (false != $link) + $msg .= "<p>Check our latest <a href=\"" . $link . "\">news!</a></p>"; + return $msg; +} + +if ($_GET['cli_debug'] == 'yes') + $cli_debug = true; + +if ($_GET['backend_test'] == 'no') +{ + $cli_debug = true; + $backend_test = false; +} + +session_start(); + +if (! isset ($_SESSION['payment_ok'])) + echo "<p>Please land here after a successful payment!</p>"; +else{ + $news = false; + switch ($_SESSION['receiver']){ + case "Taler": + $news = "https://taler.net/news"; + break; + case "GNUnet": + $news = "https://gnunet.org/"; + break; + case "Tor": + $news = "https://www.torproject.org/press/press.html.en"; + break; + } + echo generate_msg ($news); +} + +?> + </article> + </section> +</body> +</html> diff --git a/src/frontend/fullfillment.php b/src/frontend/fullfillment.php @@ -1,50 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <title>Taler's "Demo" Shop</title> - <link rel="stylesheet" type="text/css" href="style.css"> -</head> -<body> - -<?php -/* - This file is part of GNU 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 Lesser General Public License as published by the Free Software - Foundation; either version 2.1, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with - TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> - -*/ - -$cli_debug = false; -$backend_test = true; - -if ($_GET['cli_debug'] == 'yes') - $cli_debug = true; - -if ($_GET['backend_test'] == 'no') -{ - $cli_debug = true; - $backend_test = false; -} - -session_start(); - - -if (! isset ($_SESSION['payment_ok'])) - echo "Please land here after a successful payment!"; -else - echo "Thanks for donating to " . $_SESSION['receiver']; - -?> - -</body> -</html> diff --git a/src/frontend/generate_taler_contract.php b/src/frontend/generate_taler_contract.php @@ -35,10 +35,10 @@ $cli_debug = false; $backend_test = true; -if ($_GET['cli_debug'] == 'yes') +if (isset($_GET['cli_debug']) && $_GET['cli_debug'] == 'yes') $cli_debug = true; -if ($_GET['backend_test'] == 'no') +if (isset($_GET['backend_test']) && $_GET['backend_test'] == 'no') { $cli_debug = true; $backend_test = false; @@ -90,9 +90,12 @@ $teatax = array ('value' => 1, // Take a timestamp $now = new DateTime('now'); +$PAY_URL = "pay.php"; +$EXEC_URL = "execute.php"; + // pack the JSON for the contract // --- FIXME: exact format needs review! -$json = json_encode (array ('amount' => array ('value' => $amount_value, +$contract = array ('amount' => array ('value' => $amount_value, 'fraction' => $amount_fraction, 'currency' => $currency), 'max_fee' => array ('value' => 3, @@ -110,7 +113,8 @@ $json = json_encode (array ('amount' => array ('value' => $amount_value, 'delivery_date' => "Some Date Format", 'delivery_location' => 'LNAME1')), 'timestamp' => "/Date(" . $now->getTimestamp() . ")/", - 'pay_url' => "/taler/pay", + 'pay_url' => $PAY_URL, + 'exec_url' => $EXEC_URL, 'expiry' => "/Date(" . $now->add(new DateInterval('P2W'))->getTimestamp() . ")/", 'refund_deadline' => "/Date(" . $now->add(new DateInterval('P3M'))->getTimestamp() . ")/", 'merchant' => array ('address' => 'LNAME2', @@ -138,19 +142,22 @@ $json = json_encode (array ('amount' => array ('value' => $amount_value, 'state' => 'Test State', 'region' => 'Test Region', 'province' => 'Test Province', - 'ZIP code' => 4908))), JSON_PRETTY_PRINT); + 'ZIP code' => 4908))); +$json = json_encode (array ('contract' => $contract, 'exec_url' => $EXEC_URL, 'pay_url' => $PAY_URL), JSON_PRETTY_PRINT); if ($cli_debug && !$backend_test) { echo $json . "\n"; exit; } -// Craft the HTTP request, note that the backend -// could be on an entirely different machine if -// desired. -$req = new http\Client\Request ("POST", - "http://" . $_SERVER["SERVER_NAME"] . "/backend/contract", - array ("Content-Type" => "application/json")); + +$url = (new http\URL("http://".$_SERVER["HTTP_HOST"])) + ->mod(array ("path" => "backend/contract"), http\Url::JOIN_PATH); + +$req = new http\Client\Request("POST", + $url, + array ("Content-Type" => "application/json")); + $req->getBody()->append ($json); // Execute the HTTP request @@ -168,10 +175,11 @@ http_response_code ($status_code); if ($status_code != 200) { echo "Error while generating the contract"; + echo $resp->body->toString (); } else -{ - // send the contract back to the wallet without touching it +{ $got_json = json_decode ($resp->body->toString ()); + $_SESSION['H_contract'] = $got_json->H_contract; echo $resp->body->toString (); } ?> diff --git a/src/frontend/index.html b/src/frontend/index.html @@ -1,36 +1,83 @@ <!DOCTYPE html> <html lang="en"> <head> - <title>Taler's "Demo" Shop</title> + <title>Toy &quot;Store&quot; - Taler Demo</title> <link rel="stylesheet" type="text/css" href="style.css"> - <script> /* @licstart The following is the entire license notice for the - JavaScript code in this page. - - Copyright (C) 2015 GNUnet e.V. - - The JavaScript code in this page is free software: you can - redistribute it and/or modify it under the terms of the GNU - Lesser General Public License (GNU LGPL) as published by the Free Software - Foundation, either version 2.1 of the License, or (at your option) - any later version. The code is distributed WITHOUT ANY WARRANTY; - without even the implied warranty of MERCHANTABILITY or FITNESS - FOR A PARTICULAR PURPOSE. See the GNU LGPL for more details. - - As additional permission under GNU LGPL version 2.1 section 7, you - may distribute non-source (e.g., minimized or compacted) forms of - that code without the copy of the GNU LGPL normally required by - section 4, provided you include this license notice and a URL - through which recipients can access the Corresponding Source. - - @licend The above is the entire license notice - for the JavaScript code in this page. - */ - </script> + <script type="text/javascript"> + /* @licstart The following is the entire license notice for the + JavaScript code in this page. + + Copyright (C) 2015 GNUnet e.V. + + The JavaScript code in this page is free software: you can + redistribute it and/or modify it under the terms of the GNU + Lesser General Public License (GNU LGPL) as published by the Free Software + Foundation, either version 2.1 of the License, or (at your option) + any later version. The code is distributed WITHOUT ANY WARRANTY; + without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU LGPL for more details. + + As additional permission under GNU LGPL version 2.1 section 7, you + may distribute non-source (e.g., minimized or compacted) forms of + that code without the copy of the GNU LGPL normally required by + section 4, provided you include this license notice and a URL + through which recipients can access the Corresponding Source. + + @licend The above is the entire license notice + for the JavaScript code in this page. + */ + function handleInstall() { + var b = document.getElementById("main"); + b.classList.add("installed"); + }; + + function handleUninstall() { + var b = document.getElementById("main"); + b.classList.remove("installed"); + }; + + function probeTaler() { + var eve = new Event("taler-probe"); + document.dispatchEvent(eve); + } + + + // Only probe taler once the DOM is ready and + // we can manipulate it. + window.addEventListener("load", probeTaler, false); + + document.addEventListener("taler-wallet-present", handleInstall, false); + document.addEventListener("taler-unload", handleUninstall, false); + document.addEventListener("taler-load", handleInstall, false); + + </script> + <style> + *:not(.installed) .if-installed { display: none } + .installed .if-not-installed { display: none } + *:not(.installed) .if-not-installed { display: initial } + .installed .if-installed { display: initial } + </style> </head> -<body id="css-zen-garden" onload="signal_me()"> - <div class="supporting" id="welcome" role="main"> - <div class="explanation" id="zen-explanation" role="article"> - <h1>Welcome to the Taler "Demo" Shop</h1> + +<body> + <header> + <div id="logo"> + <svg height="100" width="100"> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> + </svg> + </div> + + <h1>Toy &quot;Store&quot; - Taler Demo</h1> + </header> + + <aside class="sidebar" id="left"> + </aside> + + <section id="main"> + <article> + <h1>Welcome to the Taler Demo Shop</h1> + <p>This "toy" website provides you with the ability to experience using the <a href="https://www.taler.net/">GNU Taler</a> @@ -45,152 +92,93 @@ <tt>mint.demo.taler.net</tt> and <tt>bank.demo.taler.net</tt>, correspond to a Taler mint and bank with tight Taler integration respectively. - <!-- TODO: maybe offer the wallet at 'taler.net/extension' in the - future, instead of at 'demo.taler.net/'? --> - </p> - </div> - </div> - <div class="supporting" id="instructions" role="main"> - <div class="explanation" id="installation" role="article"> - <h2>Step 1: Installing the Taler wallet (once)</h2> - <p>First, you need to install the Taler wallet browser extension. - It is currently only available for Firefox. If you run - Firefox, simply click <a href="http://demo.taler.net/extension">here</a> - to download and install the extension. You will then have to - click on "allow" and "install" dialogs shown by Firefox. - After that, the Taler logo should appear on the right side - of your navigation bar. - <!-- TODO: insert screenshot highlighting the icon? --> - </p> - </div> - <div class="explanation" id="installation-done" role="article" style="display:none;"> - <h2>Step 1: Installing the Taler wallet (done!)</h2> - <p>Congratulations, you have installed the Taler wallet correctly. - You can now proceed with the next steps. </p> - </div> - <div class="explanation" id="wire" role="article" style="display:none;"> - <h2>Step 2: Withdraw coins (occasionally)</h2> - <p>The next step is to withdraw coins, after all you cannot - pay with an empty wallet. To be allowed to withdraw - coins from a mint, you first need to transfer currency to the mint - using the normal banking system, for example by using a - wire transfer. If the bank offers a tight integration with Taler, it - may also support this directly over the home banking online interface. - <br> - For the demonstration, we have created a "bank" that - allows you to "wire" funds (in KUDOS) to the mint simply by - filling in the desired amount into a form. Naturally, when - using a real bank with real money, you would have to authenticate - and authorize the transfer. - <br> - Note that you would not do this step for each purchase or each shop. - Payment with Taler is like paying - with cash: you withdraw currency at the bank (or an ATM) and then - pay at many merchants without having to authenticate each time. - <br> - So, unless you have already done so, please go ahead and withdraw - KUDOS at the - <a href="http://bank.demo.taler.net/" target="_blank">Demo bank</a> - (opens in a new tab).</p> - </p> - </div> - <div class="explanation" id="payment" role="article" style="display:none;"> - <h2>Step 3: Shop! (as long as you have KUDOS left in the wallet)</h2> - <p>Now it is time to spend your hard earned KUDOS. - Note that we cannot really tell if you got any yet, - as your Taler wallet balance is visible to you, but - of course is hidden entirely from the shop.</p> - <p>The form that follows corresponds to the shopping - cart of a real Web shop; however, we kept it - very simple for the demonstration.</p> - <p>So, please choose a project and the amount of KUDOS you - wish to donate:</p> - <form name="tform" action="/checkout.php" method="POST"> - <div class="participation" id="fake-shop"> - <br> - <input type="radio" name="donation_receiver" value="Taler" checked="true">GNU Taler</input> - <br> - <input type="radio" name="donation_receiver" value="Tor">Tor</input> - <br> - <input type="radio" name="donation_receiver" value="GNUnet">GNUnet</input> + </article> + + <section> + + <article> + <h2>Step 1: Installing the Taler wallet</h2> + <p class="if-not-installed"> + First, you need to <a href="http://demo.taler.net/">install</a> + the Taler wallet browser extension. + </p> + <p class="if-installed"> + Congratulations, you have installed the Taler wallet correctly. + You can now proceed with the next steps. + </p> + </article> + + <article class="if-installed"> + <h2>Step 2: Withdraw coins <sup>(occasionally)</sup></h2> + + <p>The next step is to withdraw coins, after all you cannot + pay with an empty wallet. To be allowed to withdraw + coins from a mint, you first need to transfer currency to the mint + using the normal banking system, for example by using a + wire transfer. If the bank offers a tight integration with Taler, it + may also support this directly over the home banking online interface. <br> - <select id="taler-donation" name="donation_amount"> - <option value="0.1">0.1 KUDOS</option> - <option value="1.0">1 KUDOS</option> - <option value="6.0">5 KUDOS (*)</option> - <option value="10.0">10 KUDOS</option> - </select> - <input type="hidden" name="donation_currency" value="KUDOS"/> - <input type="submit" name="keyName" value="Donate!"/> + For the demonstration, we have created a "bank" that + allows you to "wire" funds (in KUDOS) to the mint simply by + filling in the desired amount into a form. Naturally, when + using a real bank with real money, you would have to authenticate + and authorize the transfer. <br> + Note that you would not do this step for each purchase or each shop. + Payment with Taler is like paying + with cash: you withdraw currency at the bank (or an ATM) and then + pay at many merchants without having to authenticate each time. <br> - </div> - </form> - <p>(*) To make it a bit more fun, the 5 KUDOS option - is deliberately implemented with a fault: the merchant will try to - make you donate 6 KUDOS instead of the 5 KUDOS you got to see. But do - not worry, you will be given the opportunity to review the - final offer from the merchant in a window secured - by the Taler extension. That way, you can spot the - error before committing to an incorrect contract.</p> - </p> - </div> - </div> - <script type="text/javascript"> - /* This function is called if/when a Wallet is installed. - It should enable the parts of the page that only make - sense after the Wallet has been loaded. - */ - function wallet_installed_cb(){ - b = document.getElementById("installation"); - b.style.display = 'none'; - b = document.getElementById("installation-done"); - b.style.display = ''; - b = document.getElementById("wire"); - b.style.display = ''; - b = document.getElementById("payment"); - b.style.display = ''; - }; + So, unless you have already done so, please go ahead and withdraw + KUDOS at the + <a href="http://bank.demo.taler.net/" target="_blank">Demo bank</a> + (opens in a new tab).</p> + </article> - function wallet_uninstalled_cb(){ - b = document.getElementById("installation"); - b.style.display = ''; - b = document.getElementById("installation-done"); - b.style.display = 'none'; - b = document.getElementById("wire"); - b.style.display = 'none'; - b = document.getElementById("payment"); - b.style.display = 'none'; - }; + <article class="if-installed"> + <h2>Step 3: Shop! <sup>(as long as you have KUDOS left)</sup></h2> - /* The merchant signals its taler-friendlyness to the client */ - function signal_me() - { - var eve = new Event('taler-checkout-probe'); - document.body.dispatchEvent(eve); - //alert("signaling"); - }; + <p>Now it is time to spend your hard earned KUDOS. + Note that we cannot really tell if you got any yet, + as your Taler wallet balance is visible to you, but + of course is hidden entirely from the shop.</p> + <p>The form that follows corresponds to the shopping + cart of a real Web shop; however, we kept it + very simple for the demonstration.</p> + <p>So, please choose a project and the amount of KUDOS you + wish to donate:</p> - function test_without_wallet(){ - wallet_installed_cb(); - } - - document.body.addEventListener("taler-unload", - wallet_uninstalled_cb, - false, false); - - /* Set up a listener to be called whenever a Wallet gets installed - so that the user is led towards the demo's steps progressively */ - document.body.addEventListener("taler-wallet-present", - wallet_installed_cb, - false, false); - - /* Setup callback to be called whenever the wallet is loaded/enabled - while the browser is already on this page */ - document.body.addEventListener("taler-load", - signal_me, - false); - </script> + <form name="tform" action="checkout.php" method="POST"> + <div class="participation" id="fake-shop"> + <br> + <input type="radio" name="donation_receiver" value="Taler" checked="true">GNU Taler</input> + <br> + <input type="radio" name="donation_receiver" value="Tor">Tor</input> + <br> + <input type="radio" name="donation_receiver" value="GNUnet">GNUnet</input> + <br> + <select id="taler-donation" name="donation_amount"> + <option value="0.1">0.1 KUDOS</option> + <option value="1.0">1 KUDOS</option> + <option value="6.0">5 KUDOS (*)</option> + <option value="10.0">10 KUDOS</option> + </select> + <input type="hidden" name="donation_currency" value="KUDOS"/> + <input type="submit" name="keyName" value="Donate!"/> + <br> + <br> + </div> + </form> + <p>(*) To make it a bit more fun, the 5 KUDOS option + is deliberately implemented with a fault: the merchant will try to + make you donate 6 KUDOS instead of the 5 KUDOS you got to see. But do + not worry, you will be given the opportunity to review the + final offer from the merchant in a window secured + by the Taler extension. That way, you can spot the + error before committing to an incorrect contract.</p> + </article> + </section> + </section> </body> </html> diff --git a/src/frontend/pay.php b/src/frontend/pay.php @@ -23,39 +23,39 @@ NOTE: 'max_fee' must be consistent with the same value indicated within the contract; thus, a "real" merchant must implement such a mapping -*/ + */ + +session_start(); $cli_debug = false; $backend_test = true; -if ($_GET['cli_debug'] == 'yes') +if (isset($_GET['cli_debug']) && $_GET['cli_debug'] == 'yes') +{ $cli_debug = true; +} -if ($_GET['backend_test'] == 'no') +if (isset($_GET['backend_test']) && $_GET['backend_test'] == 'no') { $cli_debug = true; $backend_test = false; } -session_start(); - -if (!$cli_debug && (! isset($_SESSION['receiver']))) +if (!isset($_SESSION['H_contract'])) { - http_response_code(400); - echo "Please, donate to someone before landing here!"; - exit(); + echo "No session active."; + http_response_code (301); + return; } -$cli_debug = false; -$backend_test = true; - -if ($_GET['cli_debug'] == 'yes') - $cli_debug = true; - -if ($_GET['backend_test'] == 'no') +if (isset($_SESSION['payment_ok']) && $_SESSION['payment_ok'] == true) { - $cli_debug = true; - $backend_test = false; + $_SESSION['payment_ok'] = true; + http_response_code (301); + $url = (new http\URL($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])) + ->mod(array ("path" => "fulfillment.php"), http\Url::JOIN_PATH); + header("Location: $url"); + die(); } $post_body = file_get_contents('php://input'); @@ -66,15 +66,15 @@ $edate = array ('edate' => $deposit_permission = json_decode ($post_body, true); -$to_add = array ('max_fee' => array ('value' => 3, - 'fraction' => 8, - 'currency' => $_SESSION['currency']), - 'amount' => array ('value' => $_SESSION['amount_value'], - 'fraction' => $_SESSION['amount_fraction'], - 'currency' => $_SESSION['currency'])); +$to_add = array('max_fee' => array('value' => 3, + 'fraction' => 8, + 'currency' => $_SESSION['currency']), + 'amount' => array('value' => $_SESSION['amount_value'], + 'fraction' => $_SESSION['amount_fraction'], + 'currency' => $_SESSION['currency'])); -$new_deposit_permission = array_merge ($deposit_permission, $to_add); -$new_deposit_permission_edate = array_merge ($new_deposit_permission, $edate); +$new_deposit_permission = array_merge($deposit_permission, $to_add); +$new_deposit_permission_edate = array_merge($new_deposit_permission, $edate); /* Craft the HTTP request, note that the backend could be on an entirely different machine if @@ -85,14 +85,25 @@ if ($cli_debug && !$backend_test) /* DO NOTE the newline at the end of 'echo's argument */ //echo json_encode ($new_deposit_permission_edate, JSON_PRETTY_PRINT) - echo json_encode ($new_deposit_permission, JSON_PRETTY_PRINT) + echo json_encode($new_deposit_permission, JSON_PRETTY_PRINT) . "\n"; exit; } -$req = new http\Client\Request ("POST", - "http://" . $_SERVER["SERVER_NAME"] . "/backend/pay", - array ("Content-Type" => "application/json")); + +// Backend is relative to the shop site. +/** + * WARNING: the "shop site" is '"http://".$_SERVER["HTTP_HOST"]' + * So do not attach $_SERVER["REQUEST_URI"] before proxying requests + * to the backend + */ +//$url = (new http\URL("http://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"])) +$url = (new http\URL("http://".$_SERVER["HTTP_HOST"])) + ->mod(array ("path" => "backend/pay"), http\Url::JOIN_PATH); + +$req = new http\Client\Request("POST", + $url, + array ("Content-Type" => "application/json")); $req->getBody()->append (json_encode ($new_deposit_permission)); // Execute the HTTP request @@ -112,15 +123,19 @@ if ($status_code != 200) /* error: just forwarding to the wallet what gotten from the backend (which is forwarding 'as is' the error gotten from the mint) */ + echo json_encode ($new_deposit_permission); + echo "Error came from the backend, status $status_code\n"; + echo "\n"; echo $resp->body->toString (); - } else { -$_SESSION['payment_ok'] = true; -http_response_code (301); -header("Location: http://" . $_SERVER["SERVER_NAME"] . "/fullfillment"); -die(); + $_SESSION['payment_ok'] = true; + http_response_code (301); + $url = (new http\URL($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])) + ->mod(array ("path" => "fulfillment.php"), http\Url::JOIN_PATH); + header("Location: $url"); + die(); } ?> diff --git a/src/frontend/style.css b/src/frontend/style.css @@ -0,0 +1,123 @@ +body { + background-color: white; + margin: 0; + padding: 0; + font-family: Verdana, sans; +} + +header { + width: 100%; + height: 100px; + margin: 0; + padding: 0; + border-bottom: 1px solid black; +} + +header h1 { + font-size: 200%; + margin: 0; + padding: 0 0 0 120px; + position: relative; + top: 50%; + transform: translateY(-50%); +} + +header #logo { + float: left; + width: 100px; + padding: 0; + margin: 0; + text-align: center; + border-right: 1px solid black; +} + +aside { + width: 100px; + float: left; +} + +section#main { + margin: 0 0 0 100px; + padding: 20px; + border-left: 1px solid black; + height: 100%; + max-width: 40em; +} + +section#main h1:first-child { + margin-top: 0; +} + +h1 { + font-size: 160%; +} + +h2 { + font-size: 140%; +} + +h3 { + font-size: 120%; +} + +h4, h5, h6 { + font-size: 100%; +} + +.loader { + font-size: 10px; + margin: 50px auto; + text-indent: -9999em; + width: 11em; + height: 11em; + border-radius: 50%; + background: #ffffff; + background: -moz-linear-gradient(left, #000 10%, rgba(255, 255, 255, 0) 42%); + background: -webkit-linear-gradient(left, #000 10%, rgba(255, 255, 255, 0) 42%); + background: -o-linear-gradient(left, #000 10%, rgba(255, 255, 255, 0) 42%); + background: -ms-linear-gradient(left, #000 10%, rgba(255, 255, 255, 0) 42%); + position: relative; + -webkit-animation: load3 1.4s infinite linear; + animation: load3 1.4s infinite linear; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); +} + +.loader:after { + background: #fff; + width: 75%; + height: 75%; + border-radius: 50%; + content: ''; + margin: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +@-webkit-keyframes load3 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes load3 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + + diff --git a/src/include/Makefile.am b/src/include/Makefile.am @@ -5,4 +5,5 @@ EXTRA_DIST = \ talerincludedir = $(includedir)/taler talerinclude_HEADERS = \ - taler_merchantdb_lib.h + taler_merchantdb_lib.h \ + taler_merchant_service.h diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -0,0 +1,306 @@ +/* + 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 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 include/taler_merchant_service.h + * @brief C interface of libtalermerchant, a C library to use merchant's HTTP API + * @author Christian Grothoff + */ +#ifndef _TALER_MERCHANT_SERVICE_H +#define _TALER_MERCHANT_SERVICE_H + +#include <taler/taler_util.h> + +/* ********************* event loop *********************** */ + +/** + * @brief Handle to this library context. This is where the + * main event loop logic lives. + */ +struct TALER_MERCHANT_Context; + + +/** + * Initialise a context. A context should be used for each thread and should + * not be shared among multiple threads. + * + * @return the context, NULL on error (failure to initialize) + */ +struct TALER_MERCHANT_Context * +TALER_MERCHANT_init (void); + + +/** + * 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); + + +/** + * Run the main event loop for the Taler interaction. + * + * @param ctx the library context + */ +void +TALER_MERCHANT_perform (struct TALER_MERCHANT_Context *ctx); + + +/** + * 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); + + +/* ********************* /pay *********************** */ + + +/** + * @brief Handle to a /pay operation at a merchant. Note that we use + * the same handle for interactions with frontends (API for wallets) + * or backends (API for frontends). The difference is that for the + * frontend API, we need the private keys of the coins, while for + * the backend API we need the public keys and signatures received + * from the wallet. Also, the frontend returns a redirect URI on + * success, while the backend just returns a success status code. + */ +struct TALER_MERCHANT_Pay; + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /pay request to a merchant. + * + * @param cls closure + * @param http_status HTTP response code, 200 or 300-level response codes + * can indicate success, depending on whether the interaction + * was with a merchant frontend or backend; + * 0 if the merchant's reply is bogus (fails to follow the protocol) + * @param redirect_uri URI for the redirect, if the request was successful and we were talking to a frontend; + * NULL if the request failed or if were were talking to a backend + * @param obj the received JSON reply, with error details if the request failed + */ +typedef void +(*TALER_MERCHANT_PayCallback) (void *cls, + unsigned int http_status, + const char *redirect_uri, + json_t *obj); + + +/** + * Information we need from the wallet for each coin when doing the + * payment. + */ +struct TALER_MERCHANT_PayCoin +{ + /** + * Denomination key with which the coin is signed + */ + struct TALER_DenominationPublicKey denom_pub; + + /** + * Mint’s unblinded signature of the coin + */ + struct TALER_DenominationSignature denom_sig; + + /** + * Coin's private key. + */ + struct TALER_CoinSpendPrivateKeyP coin_priv; + + /** + * Amount this coin is to contribute (including fee). + */ + struct TALER_Amount amount_with_fee; + + /** + * Amount this coin is to contribute (without fee). + */ + struct TALER_Amount amount_without_fee; +}; + + +/** + * Pay a merchant. API for wallets that have the coin's private keys. + * + * @param merchant the merchant context + * @param merchant_uri URI of the merchant + * @param mint_uri URI of the mint that the coins belong to + * @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 max_fee maximum fee covered by the merchant (according to the contract) + * @param amount total value of the contract to be paid to the merchant + * @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 char *merchant_uri, + const char *mint_uri, + 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_PayCoin *coins, + const struct TALER_Amount *max_fee, + const struct TALER_Amount *amount, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls); + + +/** + * Information we need from the frontend when forwarding + * a payment to the backend. + */ +struct TALER_MERCHANT_PaidCoin +{ + /** + * Denomination key with which the coin is signed + */ + struct TALER_DenominationPublicKey denom_pub; + + /** + * Mint’s unblinded signature of the coin + */ + struct TALER_DenominationSignature denom_sig; + + /** + * Coin's public key. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Coin's signature key. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Amount this coin is to contribute (including fee). + */ + struct TALER_Amount amount_with_fee; + + /** + * Amount this coin is to contribute (without fee). + */ + struct TALER_Amount amount_without_fee; + +}; + + +/** + * 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 merchant_uri URI of the merchant + * @param mint_uri URI of the mint that the coins belong to + * @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 execution_deadline date by which the merchant would like the mint to execute the transaction (can be zero if there is no specific date desired by the frontend) + * @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 max_fee maximum fee covered by the merchant (according to the contract) + * @param amount total value of the contract to be paid to the merchant + * @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 char *merchant_uri, + const char *mint_uri, + 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, + struct GNUNET_TIME_Absolute execution_deadline, + unsigned int num_coins, + const struct TALER_MERCHANT_PaidCoin *coins, + const struct TALER_Amount *max_fee, + const struct TALER_Amount *amount, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls); + + +/** + * Cancel a /pay request. Note that if you cancel a request like + * this, you have no assurance that the request has not yet been + * forwarded to the merchant. Thus, the payment may still succeed or + * fail. Re-issue the original /pay request to resume/retry and + * obtain a definitive result, or /refresh the coins involved to + * ensure that the merchant can no longer complete the payment. + * + * @param wh the wire information request handle + */ +void +TALER_MERCHANT_pay_cancel (struct TALER_MERCHANT_Pay *ph); + + +#endif /* _TALER_MERCHANT_SERVICE_H */ diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -69,7 +69,7 @@ struct TALER_MERCHANTDB_Plugin * @param refund refund deadline * @param amount_without_fee amount the mint will deposit * @param coin_pub public key of the coin - * @param merchant_pub our public key + * @param mint_proof proof from mint that coin was accepted * @return #GNUNET_OK on success, #GNUNET_SYSERR upon error */ int @@ -81,8 +81,21 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Absolute refund, const struct TALER_Amount *amount_without_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_MintSignatureP *mint_sig); + json_t *mint_proof); -}; + /** + * Check whether a payment has already been stored + * + * @param cls our plugin handle + * @param transaction_id the transaction id to search into + * the db + * + * @return GNUNET_OK if found, GNUNET_NO if not, GNUNET_SYSERR + * upon error + */ + int + (*check_payment) (void *cls, + uint64_t transaction_id); +}; #endif diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -0,0 +1,51 @@ +# 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) \ + -ltalermint \ + -ltalerutil \ + -lgnunetutil \ + -ljansson + +EXTRA_DIST = \ + test_merchant.conf diff --git a/src/lib/merchant_api_context.c b/src/lib/merchant_api_context.c @@ -0,0 +1,536 @@ +/* + 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) +{ + 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)); + *max_fd = GNUNET_MAX (m, *max_fd); + to = *timeout; + 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 = 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 @@ -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/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 @@ -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 @@ -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/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 @@ -0,0 +1,490 @@ +/* + 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/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; + + /** + * Reference to the merchant. + */ + struct TALER_MERCHANT_Context *merchant; +}; + + + +/** + * Function called when we're done processing the + * HTTP /pay request. + * + * @param cls the `struct TALER_MERCHANT_Pay` + * @param eh the curl request handle + */ +static void +handle_pay_finished (void *cls, + CURL *eh) +{ + /* FIXME: this function is not yet implemented!!! */ + struct TALER_MERCHANT_Pay *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, + "FIXME-redirect-URI", + 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 mint_uri URI of the mint that the coins belong to + * @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 max_fee maximum fee covered by the merchant (according to the contract) + * @param amount total value of the contract to be paid to the merchant + * @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 char *merchant_uri, + const char *mint_uri, + 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_PayCoin *coins, + const struct TALER_Amount *max_fee, + const struct TALER_Amount *amount, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls) +{ + unsigned int i; + struct TALER_DepositRequestPS dr; + struct TALER_MERCHANT_PaidCoin pc[num_coins]; + + dr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT); + dr.purpose.size = htonl (sizeof (struct TALER_DepositRequestPS)); + dr.h_contract = *h_contract; + dr.h_wire = *h_wire; + dr.timestamp = GNUNET_TIME_absolute_hton (timestamp); + dr.refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline); + dr.transaction_id = GNUNET_htonll (transaction_id); + dr.merchant = *merchant_pub; + for (i=0;i<num_coins;i++) + { + const struct TALER_MERCHANT_PayCoin *coin = &coins[i]; + struct TALER_MERCHANT_PaidCoin *p = &pc[i]; + struct TALER_Amount fee; + + /* prepare 'dr' for this coin to generate coin signature */ + GNUNET_CRYPTO_eddsa_key_get_public (&coin->coin_priv.eddsa_priv, + &dr.coin_pub.eddsa_pub); + TALER_amount_hton (&dr.amount_with_fee, + &coin->amount_with_fee); + if (GNUNET_SYSERR == + TALER_amount_subtract (&fee, + &coin->amount_with_fee, + &coin->amount_without_fee)) + { + /* Integer underflow, fee larger than total amount? + This should not happen (client violated API!) */ + GNUNET_break (0); + return NULL; + } + TALER_amount_hton (&dr.deposit_fee, + &fee); + GNUNET_CRYPTO_eddsa_sign (&coin->coin_priv.eddsa_priv, + &dr.purpose, + &p->coin_sig.eddsa_signature); + p->denom_pub = coin->denom_pub; + p->denom_sig = coin->denom_sig; + p->coin_pub = dr.coin_pub; + p->amount_with_fee = coin->amount_with_fee; + p->amount_without_fee = coin->amount_without_fee; + } + return TALER_MERCHANT_pay_frontend (merchant, + merchant_uri, + mint_uri, + h_wire, + h_contract, + timestamp, + transaction_id, + merchant_pub, + refund_deadline, + GNUNET_TIME_UNIT_ZERO_ABS, + num_coins, + pc, + max_fee, + amount, + pay_cb, + pay_cb_cls); +} + + +/** + * 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 mint_uri URI of the mint that the coins belong to + * @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 execution_deadline date by which the merchant would like the mint to execute the transaction (can be zero if there is no specific date desired by the frontend) + * @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 max_fee maximum fee covered by the merchant (according to the contract) + * @param amount total value of the contract to be paid to the merchant + * @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 char *merchant_uri, + const char *mint_uri, + 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, + struct GNUNET_TIME_Absolute execution_deadline, + unsigned int num_coins, + const struct TALER_MERCHANT_PaidCoin *coins, + const struct TALER_Amount *max_fee, + const struct TALER_Amount *amount, + TALER_MERCHANT_PayCallback pay_cb, + void *pay_cb_cls) +{ + struct TALER_MERCHANT_Pay *ph; + json_t *pay_obj; + json_t *j_coins; + CURL *eh; + struct TALER_Amount total_fee; + struct TALER_Amount total_amount; + unsigned int i; + + if (0 == num_coins) + { + GNUNET_break (0); + return NULL; + } + j_coins = json_array (); + for (i=0;i<num_coins;i++) + { + json_t *j_coin; + const struct TALER_MERCHANT_PaidCoin *pc = &coins[i]; + struct TALER_Amount fee; + + if (GNUNET_SYSERR == + TALER_amount_subtract (&fee, + &pc->amount_with_fee, + &pc->amount_without_fee)) + { + /* Integer underflow, fee larger than total amount? + This should not happen (client violated API!) */ + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } + if (0 == i) + { + total_fee = fee; + total_amount = pc->amount_with_fee; + } + else + { + if ( (GNUNET_OK != + TALER_amount_add (&total_fee, + &total_fee, + &fee)) || + (GNUNET_OK != + TALER_amount_add (&total_amount, + &total_amount, + &pc->amount_with_fee)) ) + { + /* integer overflow */ + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } + } + + /* create JSON for this coin */ + j_coin = json_pack ("{s:o, s:o," /* f/coin_pub */ + " s:o, s:o," /* denom_pub / ub_sig */ + " s:o}", /* coin_sig */ + "f", TALER_json_from_amount (&pc->amount_with_fee), + "coin_pub", TALER_json_from_data (&pc->coin_pub, + sizeof (struct TALER_CoinSpendPublicKeyP)), + "denom_pub", TALER_json_from_rsa_public_key (pc->denom_pub.rsa_public_key), + "ub_sig", TALER_json_from_rsa_signature (pc->denom_sig.rsa_signature), + "coin_sig", TALER_json_from_data (&pc->coin_sig, + sizeof (struct TALER_CoinSpendSignatureP)) + ); + json_array_append (j_coins, + j_coin); + } + + { /* Sanity check that total_amount and total_fee + match amount/max_fee requirements */ + struct TALER_Amount fee_left; + + if (GNUNET_OK == + TALER_amount_subtract (&fee_left, + &total_fee, + max_fee)) + { + /* Wallet must cover part of the fee! */ + struct TALER_Amount new_amount; + + if (GNUNET_OK != + TALER_amount_add (&new_amount, + &fee_left, + amount)) + { + /* integer overflow */ + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } + if (1 == + TALER_amount_cmp (&new_amount, + &total_amount)) + { + /* new_amount > total_amount: all of the coins (total_amount) + do not add up to at least the new_amount owed to the + merchant, this request is bogus, abort */ + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } + } + else + { + /* Full fee covered by merchant, but our total + must at least cover the total contract amount */ + if (1 == + TALER_amount_cmp (amount, + &total_amount)) + { + /* amount > total_amount: all of the coins (total_amount) do + not add up to at least the amount owed to the merchant, + this request is bogus, abort */ + GNUNET_break (0); + json_decref (j_coins); + return NULL; + } + } + } /* end of sanity check */ + pay_obj = json_pack ("{s:o, s:o," /* H_wire/H_contract */ + " s:I, s:o," /* transaction id, timestamp */ + " s:o, s:s," /* refund_deadline, mint */ + " s:o, s:o," /* coins, max_fee */ + " s:o}", /* amount */ + "H_wire", TALER_json_from_data (&h_wire, + sizeof (h_wire)), + "H_contract", TALER_json_from_data (h_contract, + sizeof (struct GNUNET_HashCode)), + "transaction_id", (json_int_t) transaction_id, + "timestamp", TALER_json_from_abs (timestamp), + "refund_deadline", TALER_json_from_abs (refund_deadline), + "mint", mint_uri, + "coins", j_coins, + "max_fee", TALER_json_from_amount (max_fee), + "amount", TALER_json_from_amount (amount) + ); + + if (0 != execution_deadline.abs_value_us) + { + /* Frontend did have an execution date in mind, add it */ + json_object_set_new (pay_obj, + "edate", + TALER_json_from_abs (execution_deadline)); + } + + ph = GNUNET_new (struct TALER_MERCHANT_Pay); + ph->merchant = merchant; + ph->cb = pay_cb; + ph->cb_cls = pay_cb_cls; + ph->url = GNUNET_strdup (merchant_uri); + + 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)); + ph->job = MAC_job_add (merchant, + eh, + GNUNET_YES, + &handle_pay_finished, + ph); + return ph; +} + + +/** + * 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_Pay *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-mint-home/config/mint-common.conf b/src/lib/test-mint-home/config/mint-common.conf @@ -0,0 +1,30 @@ +[mint] +# Currency supported by the mint (can only be one) +CURRENCY = EUR + +# Wire format supported by the mint +# We use 'test' for testing of the actual +# coin operations, and 'sepa' to test SEPA-specific routines. +WIREFORMAT = test sepa + +# HTTP port the mint listens to +PORT = 8081 + +# Master public key used to sign the mint's various keys +MASTER_PUBLIC_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG + +# How to access our database +DB = postgres + +# Is this is a testcase, use transient DB actions? +TESTRUN = YES + +[mintdb-postgres] + +DB_CONN_STR = "postgres:///talercheck" + +[mint-wire-sepa] +SEPA_RESPONSE_FILE = "test-mint-home/sepa.json" + +[mint-wire-test] +REDIRECT_URL = "http://www.taler.net/" diff --git a/src/lib/test-mint-home/config/mint-keyup.conf b/src/lib/test-mint-home/config/mint-keyup.conf @@ -0,0 +1,86 @@ +[mint_keys] + +# how long is one signkey valid? +signkey_duration = 4 weeks + +# how long are the signatures with the signkey valid? +legal_duration = 2 years + +# how long do we generate denomination and signing keys +# ahead of time? +lookahead_sign = 32 weeks 1 day + +# how long do we provide to clients denomination and signing keys +# ahead of time? +lookahead_provide = 4 weeks 1 day + + +# Coin definitions are detected because the section +# name begins with "coin_". The rest of the +# name is free, but of course following the convention +# of "coin_$CURRENCY[_$SUBUNIT]_$VALUE" make sense. +[coin_eur_ct_1] +value = EUR:0.01 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.00 +fee_deposit = EUR:0.00 +fee_refresh = EUR:0.01 +rsa_keysize = 1024 + +[coin_eur_ct_10] +value = EUR:0.10 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +rsa_keysize = 1024 + +[coin_eur_1] +value = EUR:1 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +rsa_keysize = 1024 + +[coin_eur_5] +value = EUR:5 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +rsa_keysize = 1024 + +[coin_eur_10] +value = EUR:10 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +rsa_keysize = 1024 + +[coin_eur_1000] +value = EUR:1000 +duration_overlap = 5 minutes +duration_withdraw = 7 days +duration_spend = 2 years +duration_legal = 3 years +fee_withdraw = EUR:0.01 +fee_deposit = EUR:0.01 +fee_refresh = EUR:0.03 +rsa_keysize = 2048 diff --git a/src/lib/test-mint-home/master.priv b/src/lib/test-mint-home/master.priv @@ -0,0 +1 @@ +pÚ^ó-Ú33ˆ€XXÁ!ˆ\0qúýµmUþ_‰ˆ +\ No newline at end of file diff --git a/src/lib/test-mint-home/sepa.json b/src/lib/test-mint-home/sepa.json @@ -0,0 +1,6 @@ +{ + "receiver_name": "Max Mustermann", + "iban": "DE89370400440532013000", + "bic": "COBADEFF370", + "sig": "8M5YJXM68PRAXKH76HYEBCJW657B23JA0RFGNDMZK2379YZMT626H1BN89KC0M1KJBWGYEN5Z763Q0Y7MCTZQ6BPPT7D9KFCTW60C10" +} +\ No newline at end of file diff --git a/src/lib/test_merchant.conf b/src/lib/test_merchant.conf @@ -0,0 +1,55 @@ +# Sample configuration file for a merchant. +[merchant] + +# Which port do we run the backend on? (HTTP server) +PORT = 8082 + +# FIXME: is this one used? +HOSTNAME = localhost + +# Where is our private key? +KEYFILE = test_merchant.priv + +# What currency does this backend accept? +CURRENCY = EUR + +# FIXME: to be revised +TRUSTED_MINTS = taler + +# How quickly do we want the mint to send us our money? +# Used only if the frontend does not specify a value. +# FIXME: EDATE is a bit short, 'execution_delay'? +EDATE = 3 week + +# Which plugin (backend) do we use for the DB. +DB = postgres + +[mint-taler] +URI = http://localhost:8081/ +MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG + +# Auditors must be in sections "auditor-", the rest of the section +# name could be anything. +[auditor-ezb] +# Informal name of the auditor. Just for the user. +NAME = European Central Bank + +# URI of the auditor (especially for in the future, when the +# auditor offers an automated issue reporting system). +# Not really used today. +URI = http://taler.ezb.eu/ + +# This is the important bit: the signing key of the auditor. +PUBLIC_KEY = 9QXF7XY7E9VPV47B5Z806NDFSX2VJ79SVHHD29QEQ3BG31ANHZ60 + +# This specifies which database we use. +[merchantdb-postgres] +CONFIG = postgres:///talercheck + +# "wire-" sections include wire details, here for SEPA. +[wire-sepa] +IBAN = DE67830654080004822650 +NAME = GNUNET E.V +BIC = GENODEF1SRL +SALT = 17919252168512238964 +ADDRESS = "Garching" diff --git a/src/lib/test_merchant.priv b/src/lib/test_merchant.priv @@ -0,0 +1 @@ +`ì&-Èí–ñ./öÀ¿ jxÌGÝ¢O:6l,ζXT4í +\ No newline at end of file diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c @@ -0,0 +1,1583 @@ +/* + 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/taler_util.h> +#include <taler/taler_signatures.h> +#include <taler/taler_mint_service.h> +#include "taler_merchant_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> + +/** + * URI under which the merchant is reachable during the testcase. + */ +#define MERCHANT_URI "http://localhost:8082/" + +/** + * URI under which the mint is reachable during the testcase. + */ +#define MINT_URI "http://localhost:8081/" + +/** + * 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; + +/** + * Public key of the merchant, matches the private + * key from "test_merchant.priv". + */ +static struct TALER_MerchantPublicKeyP merchant_pub; + +/** + * 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_PAY command. + * FIXME: support tests where we pay with multiple coins at once. + */ + struct + { + + /** + * Amount to pay (total for the entire contract). + */ + const char *total_amount; + + /** + * Maximum fee covered by merchant. + */ + const char *max_fee; + + /** + * Reference to a reserve_withdraw operation for a coin to + * be used for the /deposit operation. + */ + const char *coin_ref; + + /** + * If this @e coin_ref refers to an operation that generated + * an array of coins, this value determines which coin to use. + */ + unsigned int coin_idx; + + /** + * Amount to pay (from the coin, including fee). + */ + const char *amount_with_fee; + + /** + * Amount to pay (from the coin, excluding fee). The sum of the + * deltas between all @e amount_with_fee and the @e + * amount_without_fee must be less than max_fee, and the sum of + * the @e amount_with_fee must be larger than the @e + * total_amount. + */ + const char *amount_without_fee; + + /** + * 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; + + /** + * Deposit handle while operation is running. + */ + struct TALER_MERCHANT_Pay *ph; + + } 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: + 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.pay.ph = NULL; + if (cmd->expected_response_code != http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u to command %s\n", + http_status, + cmd->label); + json_dumpf (obj, stderr, 0); + fail (is); + return; + } + 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; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Interpreter runs command %u/%s(%u)\n", + is->ip, + cmd->label, + cmd->oc); + switch (cmd->oc) + { + case OC_END: + result = GNUNET_OK; + 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: + { + struct TALER_MERCHANT_PayCoin pc; + struct TALER_Amount amount; + struct TALER_Amount max_fee; + json_t *wire; + json_t *contract; + struct GNUNET_HashCode h_wire; + struct GNUNET_HashCode h_contract; + struct GNUNET_TIME_Absolute refund_deadline; + struct GNUNET_TIME_Absolute timestamp; + + /* get amount */ + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.pay.total_amount, + &amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse total amount `%s' at %u\n", + cmd->details.pay.total_amount, + is->ip); + fail (is); + return; + } + + /* get max_fee */ + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.pay.max_fee, + &max_fee)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse max_fee `%s' at %u\n", + cmd->details.pay.max_fee, + is->ip); + fail (is); + return; + } + + /* parse wire details */ + wire = json_loads (cmd->details.pay.wire_details, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == wire) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse wire details `%s' at %u\n", + cmd->details.pay.wire_details, + is->ip); + fail (is); + return; + } + TALER_hash_json (wire, + &h_wire); + json_decref (wire); + + /* parse contract */ + contract = json_loads (cmd->details.pay.contract, + JSON_REJECT_DUPLICATES, + NULL); + if (NULL == contract) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse contract details `%s' at instruction %u\n", + cmd->details.pay.contract, + is->ip); + fail (is); + return; + } + TALER_hash_json (contract, + &h_contract); + json_decref (contract); + + /* initialize 'pc' (FIXME: to do in a loop later...) */ + { + const struct Command *ref; + + memset (&pc, 0, sizeof (pc)); + ref = find_command (is, + cmd->details.pay.coin_ref); + GNUNET_assert (NULL != ref); + switch (ref->oc) + { + case OC_WITHDRAW_SIGN: + pc.coin_priv = ref->details.reserve_withdraw.coin_priv; + pc.denom_pub = ref->details.reserve_withdraw.pk->key; + pc.denom_sig = ref->details.reserve_withdraw.sig; + break; + default: + GNUNET_assert (0); + } + + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.pay.amount_without_fee, + &pc.amount_without_fee)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse amount `%s' at %u\n", + cmd->details.pay.amount_without_fee, + is->ip); + fail (is); + return; + } + + if (GNUNET_OK != + TALER_string_to_amount (cmd->details.pay.amount_with_fee, + &pc.amount_with_fee)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse amount `%s' at %u\n", + cmd->details.pay.amount_with_fee, + is->ip); + fail (is); + return; + } + } + + if (0 == cmd->details.pay.refund_deadline.rel_value_us) + refund_deadline = GNUNET_TIME_UNIT_ZERO_ABS; /* no refunds */ + else + refund_deadline = GNUNET_TIME_relative_to_absolute (cmd->details.pay.refund_deadline); + TALER_round_abs_time (&refund_deadline); + timestamp = GNUNET_TIME_absolute_get (); + TALER_round_abs_time (&timestamp); + cmd->details.pay.ph + = TALER_MERCHANT_pay_wallet (merchant, + MERCHANT_URI "pay", + MINT_URI, + &h_wire, + &h_contract, + timestamp, + cmd->details.pay.transaction_id, + &merchant_pub, + refund_deadline, + 1 /* num_coins */, + &pc /* coins */, + &max_fee, + &amount, + &pay_cb, + is); + } + if (NULL == cmd->details.pay.ph) + { + 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; + } + 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; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Shutdown executing\n"); + for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) + { + switch (cmd->oc) + { + case OC_END: + GNUNET_assert (0); + break; + case OC_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: + if (NULL != cmd->details.pay.ph) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Command %u (%s) did not complete\n", + i, + cmd->label); + TALER_MERCHANT_pay_cancel (cmd->details.pay.ph); + cmd->details.pay.ph = NULL; + } + break; + 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 != merchant) + { + TALER_MERCHANT_fini (merchant); + merchant = 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 */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Certificate callback invoked, starting interpreter\n"); + 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; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Event loop running\n"); + TALER_MINT_perform (ctx); + TALER_MERCHANT_perform (merchant); + 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); + TALER_MERCHANT_get_select_info (merchant, + &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.pay.total_amount = "EUR:5", + .details.pay.max_fee = "EUR:0.5", + .details.pay.coin_ref = "withdraw-coin-1", + .details.pay.amount_with_fee = "EUR:5", + .details.pay.amount_without_fee = "EUR:4.99", + .details.pay.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }", + .details.pay.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.pay.total_amount = "EUR:5", + .details.pay.max_fee = "EUR:0.5", + .details.pay.coin_ref = "withdraw-coin-1", + .details.pay.amount_with_fee = "EUR:5", + .details.pay.amount_without_fee = "EUR:4.99", + .details.pay.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":43 }", + .details.pay.contract = "{ \"items\":[{ \"name\":\"ice cream\", \"value\":1} ] }", + .details.pay.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.pay.total_amount = "EUR:5", + .details.pay.max_fee = "EUR:0.5", + .details.pay.coin_ref = "withdraw-coin-1", + .details.pay.amount_with_fee = "EUR:5", + .details.pay.amount_without_fee = "EUR:4.99", + .details.pay.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }", + .details.pay.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.pay.total_amount = "EUR:5", + .details.pay.max_fee = "EUR:0.5", + .details.pay.coin_ref = "withdraw-coin-1", + .details.pay.amount_with_fee = "EUR:5", + .details.pay.amount_without_fee = "EUR:4.99", + .details.pay.wire_details = "{ \"type\":\"TEST\", \"bank\":\"dest bank\", \"account\":42 }", + .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":2} ] }", + .details.pay.transaction_id = 1 }, + + { .oc = OC_END } + }; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Interpreter initializing\n"); + is = GNUNET_new (struct InterpreterState); + is->commands = commands; + + ctx = TALER_MINT_init (); + GNUNET_assert (NULL != ctx); + merchant = TALER_MERCHANT_init (); + GNUNET_assert (NULL != merchant); + ctx_task = GNUNET_SCHEDULER_add_now (&context_task, + ctx); + mint = TALER_MINT_connect (ctx, + MINT_URI, + &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) +{ + /* Value from "gnunet-ecc -p test_merchant.priv" */ + const char *merchant_pub_str + = "5TRNSWAWHKBJ7G4T3PKRCQA6MCB3MX82F4M2XXS1653KE1V8RFPG"; + struct GNUNET_OS_Process *proc; + struct GNUNET_OS_Process *mintd; + struct GNUNET_OS_Process *merchantd; + + GNUNET_log_setup ("test-merchant-api", + "WARNING", + NULL); + GNUNET_assert (GNUNET_OK == + GNUNET_STRINGS_string_to_data (merchant_pub_str, + strlen (merchant_pub_str), + &merchant_pub, + sizeof (merchant_pub))); + 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); + /* 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 " MINT_URI "keys -o /dev/null -O /dev/null")); + fprintf (stderr, "\n"); + merchantd = GNUNET_OS_start_process (GNUNET_NO, + GNUNET_OS_INHERIT_STD_ALL, + NULL, NULL, NULL, + "taler-merchant-httpd", + "taler-merchant-httpd", + "-c", "test_merchant.conf", + NULL); + /* give child time to start and bind against the socket */ + fprintf (stderr, "Waiting for taler-merchant-httpd to be ready"); + do + { + fprintf (stderr, "."); + sleep (1); + } + while (0 != system ("wget -q -t 1 -T 1 " MERCHANT_URI " -o /dev/null -O /dev/null")); + fprintf (stderr, "\n"); + result = GNUNET_SYSERR; + GNUNET_SCHEDULER_run (&run, NULL); + GNUNET_OS_process_kill (merchantd, + SIGTERM); + GNUNET_OS_process_wait (merchantd); + GNUNET_OS_process_destroy (merchantd); + 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 */ diff --git a/src/tsconfig.json b/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es6", + "jsx": "react" + }, + "files": [ + "frontend/execute.tsx" + ] +} +