merchant

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

commit 78e7999e02844c13644f79ff7ef250c0de844771
parent 3f1cb92782e404d907e4337393d92d8157acd4e7
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  1 Nov 2017 16:01:59 +0100

towards implementing /tip-pickup (incomplete)

Diffstat:
Msrc/backend/taler-merchant-httpd_tip-authorize.c | 1+
Msrc/backend/taler-merchant-httpd_tip-pickup.c | 266++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/backenddb/plugin_merchantdb_postgres.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/backenddb/test_merchantdb.c | 25+++++++++++++++++++++++--
Msrc/include/taler_merchantdb_plugin.h | 18++++++++++++++++++
5 files changed, 362 insertions(+), 8 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_tip-authorize.c b/src/backend/taler-merchant-httpd_tip-authorize.c @@ -155,6 +155,7 @@ MH_handler_tip_authorize (struct TMH_RequestHandler *rh, justification, &amount, &mi->tip_reserve, + mi->tip_exchange, &expiration, &tip_id); if (TALER_EC_NONE != ec) diff --git a/src/backend/taler-merchant-httpd_tip-pickup.c b/src/backend/taler-merchant-httpd_tip-pickup.c @@ -18,8 +18,190 @@ * @brief implementation of /tip-pickup handler * @author Christian Grothoff */ +#include "platform.h" #include <microhttpd.h> +#include <jansson.h> +#include <taler/taler_json_lib.h> #include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_parsing.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_responses.h" +#include "taler-merchant-httpd_tip-pickup.h" + + +/** + * Details about a planchet that the customer wants to obtain + * a withdrawal authorization. This is the information that + * will need to be sent to the exchange to obtain the blind + * signature required to turn a planchet into a coin. + */ +struct PlanchetDetail +{ + /** + * Hash of the denomination public key. + */ + struct GNUNET_HashCode denom_pub_hash; + + /** + * Blinded coin (see GNUNET_CRYPTO_rsa_blind()). Note: is malloc()'ed! + */ + char *coin_ev; + + /** + * Number of bytes in @a coin_ev. + */ + size_t coin_ev_size; +}; + + +/** + * Prepare (and eventually execute) a pickup. Computes + * the "pickup ID" (by hashing the planchets and denomination keys), + * resolves the denomination keys and calculates the total + * amount to be picked up. Then runs the pick up execution logic. + * + * @param connection MHD connection for sending the response + * @param tip_id which tip are we picking up + * @param planchets what planchets are to be signed blindly + * @param planchets_len length of the @a planchets array + * @return #MHD_YES if a response was generated, #MHD_NO if + * the connection ought to be dropped + */ +static int +prepare_pickup (struct MHD_Connection *connection, + const struct GNUNET_HashCode *tip_id, + const struct PlanchetDetail *planchets, + unsigned int planchets_len) +{ +#if 0 + char *exchange_uri; + enum TALER_ErrorCode ec; + struct GNUNET_HashCode pickup_id; + struct TALER_ReservePrivateKeyP reserve_priv; + + ec = db->lookup_exchange_by_tip (db->cls, + tip_id, + &exchange_uri); + // FIXME: error handling + // FIXME: obtain exchange handle -- asynchronously!? => API bad! + + // Then resolve hashes to DK pubs and amounts + for (unsigned int i=0;i<planchets_len;i++) + { + } + // Total up the amounts & compute pickup_id + + ec = db->pickup_tip (db->cls, + &total, + tip_id, + &pickup_id, + &reserve_priv); + // FIXME: error handling + + // build and sign withdraw orders! + + // build final response... + + if (TALER_EC_NONE != ec) + { + /* FIXME: be more specific in choice of HTTP status code */ + return TMH_RESPONSE_reply_internal_error (connection, + ec, + "Database error approving tip"); + } + return TMH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:s}", + "status", "ok"); + +#endif + + return MHD_NO; +} + + +/** + * Information we keep for individual calls + * to requests that parse JSON, but keep no other state. + */ +struct TMH_JsonParseContext +{ + + /** + * This field MUST be first. + * FIXME: Explain why! + */ + struct TM_HandlerContext hc; + + /** + * Placeholder for #TMH_PARSE_post_json() to keep its internal state. + */ + void *json_parse_context; +}; + + +/** + * Custom cleanup routine for a `struct TMH_JsonParseContext`. + * + * @param hc the `struct TMH_JsonParseContext` to clean up. + */ +static void +json_parse_cleanup (struct TM_HandlerContext *hc) +{ + struct TMH_JsonParseContext *jpc = (struct TMH_JsonParseContext *) hc; + + TMH_PARSE_post_cleanup_callback (jpc->json_parse_context); + GNUNET_free (jpc); +} + + +/** + * Free the memory used by the planchets in the @a pd array + * (but not the @a pd array itself). + * + * @param pd array of planchets + * @param pd_len length of @a pd array + */ +static void +free_planchets (struct PlanchetDetail *pd, + unsigned int pd_len) +{ + for (unsigned int i=0;i<pd_len;i++) + GNUNET_free (pd[i].coin_ev); +} + + +/** + * Parse the given @a planchet into the @a pd. + * + * @param connection connection to use for error reporting + * @param planchet planchet data in JSON format + * @param[out] pd where to store the binary data + * @return #GNUNET_OK upon success, #GNUNET_NO if a response + * was generated, #GNUNET_SYSERR to drop the connection + */ +static int +parse_planchet (struct MHD_Connection *connection, + const json_t *planchet, + struct PlanchetDetail *pd) +{ + int ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &pd->denom_pub_hash), + GNUNET_JSON_spec_varsize ("coin_ev", + (void **) &pd->coin_ev, + &pd->coin_ev_size), + GNUNET_JSON_spec_end() + }; + + ret = TMH_PARSE_json_data (connection, + planchet, + spec); + return ret; +} + /** * Manages a /tip-pickup call, checking that the tip is authorized, @@ -39,6 +221,86 @@ MH_handler_tip_pickup (struct TMH_RequestHandler *rh, const char *upload_data, size_t *upload_data_size) { - GNUNET_break (0); /* #5099: not implemented */ - return MHD_NO; + int res; + struct GNUNET_HashCode tip_id; + json_t *planchets; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("tip_id", &tip_id), + GNUNET_JSON_spec_json ("planchets", &planchets), + GNUNET_JSON_spec_end() + }; + struct TMH_JsonParseContext *ctx; + json_t *root; + unsigned int num_planchets; + + if (NULL == *connection_cls) + { + ctx = GNUNET_new (struct TMH_JsonParseContext); + ctx->hc.cc = &json_parse_cleanup; + *connection_cls = ctx; + } + else + { + ctx = *connection_cls; + } + res = TMH_PARSE_post_json (connection, + &ctx->json_parse_context, + upload_data, + upload_data_size, + &root); + if (GNUNET_SYSERR == res) + return MHD_NO; + /* the POST's body has to be further fetched */ + if ( (GNUNET_NO == res) || + (NULL == root) ) + return MHD_YES; + + res = TMH_PARSE_json_data (connection, + root, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + json_decref (root); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + num_planchets = json_array_size (planchets); + if (num_planchets > 1024) + { + GNUNET_JSON_parse_free (spec); + json_decref (root); + /* FIXME: define proper ec for this! */ + return TMH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_BAD_REQUEST, + "{s:s}", + "status", "too many planchets"); + } + { + struct PlanchetDetail pd[num_planchets]; + + for (unsigned int i=0;i<num_planchets;i++) + { + if (GNUNET_OK != + (res = parse_planchet (connection, + json_array_get (planchets, + i), + &pd[i]))) + { + free_planchets (pd, + i); + GNUNET_JSON_parse_free (spec); + json_decref (root); + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + } + res = prepare_pickup (connection, + &tip_id, + pd, + num_planchets); + free_planchets (pd, + num_planchets); + } + GNUNET_JSON_parse_free (spec); + json_decref (root); + return res; } diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -202,6 +202,7 @@ postgres_initialize (void *cls) GNUNET_PQ_make_execute ("CREATE TABLE IF NOT EXISTS merchant_tips (" " reserve_priv BYTEA NOT NULL CHECK (LENGTH(reserve_priv)=32)" ",tip_id BYTEA NOT NULL CHECK (LENGTH(tip_id)=64)" + ",exchange_uri VARCHAR NOT NULL" ",justification VARCHAR NOT NULL" ",timestamp INT8 NOT NULL" ",amount_val INT8 NOT NULL" /* overall tip amount */ @@ -546,6 +547,7 @@ postgres_initialize (void *cls) "INSERT INTO merchant_tips" "(reserve_priv" ",tip_id" + ",exchange_uri" ",justification" ",timestamp" ",amount_val" @@ -555,8 +557,8 @@ postgres_initialize (void *cls) ",left_frac" ",left_curr" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - 10), + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + 11), GNUNET_PQ_make_prepare ("lookup_reserve_by_tip_id", "SELECT" " reserve_priv" @@ -575,6 +577,12 @@ postgres_initialize (void *cls) " WHERE pickup_id=$1" " AND tip_id=$2", 2), + GNUNET_PQ_make_prepare ("find_exchange_by_tip", + "SELECT" + " exchange_uri" + " FROM merchant_tips" + " WHERE tip_id=$1", + 1), GNUNET_PQ_make_prepare ("update_tip_balance", "UPDATE merchant_tips SET" " left_val=$2" @@ -2753,6 +2761,7 @@ postgres_enable_tip_reserve (void *cls, * @param justification why was the tip approved * @param amount how high is the tip (with fees) * @param reserve_priv which reserve is debited + * @param exchange_uri which exchange manages the tip * @param[out] expiration set to when the tip expires * @param[out] tip_id set to the unique ID for the tip * @return taler error code @@ -2768,6 +2777,7 @@ postgres_authorize_tip (void *cls, const char *justification, const struct TALER_Amount *amount, const struct TALER_ReservePrivateKeyP *reserve_priv, + const char *exchange_uri, struct GNUNET_TIME_Absolute *expiration, struct GNUNET_HashCode *tip_id) { @@ -2853,6 +2863,7 @@ postgres_authorize_tip (void *cls, struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (reserve_priv), GNUNET_PQ_query_param_auto_from_type (tip_id), + GNUNET_PQ_query_param_string (exchange_uri), GNUNET_PQ_query_param_string (justification), GNUNET_PQ_query_param_absolute_time (&now), TALER_PQ_query_param_amount (amount), /* overall amount */ @@ -2882,6 +2893,46 @@ postgres_authorize_tip (void *cls, /** + * Find out which exchange was associated with @a tip_id + * + * @param cls closure, typically a connection to the d + * @param tip_id the unique ID for the tip + * @param[out] exchange_uri set to the URI of the exchange + * @return transaction status, usually + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT for success + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if @a credit_uuid already known + */ +static enum GNUNET_DB_QueryStatus +postgres_lookup_exchange_by_tip (void *cls, + const struct GNUNET_HashCode *tip_id, + char **exchange_uri) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (tip_id), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("exchange_uri", + exchange_uri), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "find_exchange_by_tip", + params, + rs); + if (0 >= qs) + { + *exchange_uri = NULL; + return qs; + } + return qs; +} + + +/** * Pickup a tip over @a amount using pickup id @a pickup_id. * * @param cls closure, typically a connection to the db @@ -2941,8 +2992,8 @@ postgres_pickup_tip (void *cls, if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) return TALER_EC_TIP_PICKUP_TIP_ID_UNKNOWN; return (GNUNET_DB_STATUS_HARD_ERROR == qs) - ? TALER_EC_TIP_PICKUP_DB_ERROR_HARD - : TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; + ? TALER_EC_TIP_PICKUP_DB_ERROR_HARD + : TALER_EC_TIP_PICKUP_DB_ERROR_SOFT; } /* Check if pickup_id already exists */ @@ -3133,6 +3184,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->mark_proposal_paid = &postgres_mark_proposal_paid; plugin->enable_tip_reserve = &postgres_enable_tip_reserve; plugin->authorize_tip = &postgres_authorize_tip; + plugin->lookup_exchange_by_tip = &postgres_lookup_exchange_by_tip; plugin->pickup_tip = &postgres_pickup_tip; plugin->start = postgres_start; plugin->commit = postgres_commit; diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -528,20 +528,21 @@ test_tipping () struct TALER_Amount total; struct TALER_Amount amount; struct TALER_Amount inc; + char *uri; RND_BLK (&tip_reserve_priv); - if (TALER_EC_TIP_AUTHORIZE_RESERVE_UNKNOWN != + if (TALER_EC_TIP_AUTHORIZE_RESERVE_NOT_ENABLED != plugin->authorize_tip (plugin->cls, "testing tips reserve unknown", &amount, &tip_reserve_priv, + "http://localhost:8081/", &tip_expiration, &tip_id)) { GNUNET_break (0); return GNUNET_SYSERR; } - RND_BLK (&tip_credit_uuid); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (CURRENCY ":5", @@ -578,6 +579,7 @@ test_tipping () "testing tips too late", &amount, &tip_reserve_priv, + "http://localhost:8081/", &tip_expiration, &tip_id)) { @@ -623,6 +625,7 @@ test_tipping () "testing tips", &amount, &tip_reserve_priv, + "http://localhost:8081/", &tip_expiration, &tip_id)) { @@ -634,11 +637,28 @@ test_tipping () GNUNET_break (0); return GNUNET_SYSERR; } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + plugin->lookup_exchange_by_tip (plugin->cls, + &tip_id, + &uri)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 != strcmp ("http://localhost:8081/", + uri)) + { + GNUNET_free (uri); + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_free (uri); if (TALER_EC_NONE != plugin->authorize_tip (plugin->cls, "testing tips more", &amount, &tip_reserve_priv, + "http://localhost:8081/", &tip_expiration, &tip_id)) { @@ -711,6 +731,7 @@ test_tipping () "testing tips insufficient funds", &amount, &tip_reserve_priv, + "http://localhost:8081/", &tip_expiration, &tip_id)) { diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -656,6 +656,7 @@ struct TALER_MERCHANTDB_Plugin * @param justification why was the tip approved * @param amount how high is the tip (with fees) * @param reserve_priv which reserve is debited + * @param exchange_uri which exchange manages the tip * @param[out] expiration set to when the tip expires * @param[out] tip_id set to the unique ID for the tip * @return transaction status, @@ -671,11 +672,28 @@ struct TALER_MERCHANTDB_Plugin const char *justification, const struct TALER_Amount *amount, const struct TALER_ReservePrivateKeyP *reserve_priv, + const char *exchange_uri, struct GNUNET_TIME_Absolute *expiration, struct GNUNET_HashCode *tip_id); /** + * Find out which exchange was associated with @a tip_id + * + * @param cls closure, typically a connection to the d + * @param tip_id the unique ID for the tip + * @param[out] exchange_uri set to the URI of the exchange + * @return transaction status, usually + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT for success + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if @a credit_uuid already known + */ + enum GNUNET_DB_QueryStatus + (*lookup_exchange_by_tip)(void *cls, + const struct GNUNET_HashCode *tip_id, + char **exchange_uri); + + + /** * Pickup a tip over @a amount using pickup id @a pickup_id. * * @param cls closure, typically a connection to the db