-implement more of the KYC handlers

This commit is contained in:
Christian Grothoff 2021-10-19 21:02:04 +02:00
parent fa30a132a5
commit 778a402d07
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
15 changed files with 622 additions and 49 deletions

@ -1 +1 @@
Subproject commit ca56accac72b6ce050a38d36172390b14100a538
Subproject commit 8c7d9be40ba627348da3e01b91b4f1d3cc78631f

View File

@ -37,7 +37,7 @@
* @param body JSON body to add to @e ctx
* @return #GNUNET_OK on success #GNUNET_SYSERR on failure
*/
int
enum GNUNET_GenericReturnValue
TALER_curl_easy_post (struct TALER_CURL_PostContext *ctx,
CURL *eh,
const json_t *body)

View File

@ -123,6 +123,7 @@ taler_exchange_httpd_LDADD = \
-lgnunetutil \
-lgnunetjson \
-ljansson \
-lcurl \
-lz \
$(XLIB)

View File

@ -102,3 +102,7 @@ KYC_MODE = NONE
# KYC Client secret used to obtain access tokens.
# KYC_OAUTH2_CLIENT_SECRET =
# Where to redirect clients after successful
# authorization?
# KYC_OAUTH_POST_URL = https://bank.com/

View File

@ -102,6 +102,11 @@ struct TALER_EXCHANGEDB_Plugin *TEH_plugin;
*/
char *TEH_currency;
/**
* Our base URL.
*/
char *TEH_base_url;
/**
* Default timeout in seconds for HTTP requests.
*/
@ -134,6 +139,17 @@ static unsigned long long req_count;
*/
static unsigned long long req_max;
/**
* Context for all CURL operations (useful to the event loop)
*/
struct GNUNET_CURL_Context *TEH_curl_ctx;
/**
* Context for integrating #exchange_curl_ctx with the
* GNUnet event loop.
*/
static struct GNUNET_CURL_RescheduleContext *exchange_curl_rc;
/**
* Signature of functions that handle operations on coins.
@ -1203,6 +1219,19 @@ parse_kyc_oauth_cfg (void)
return GNUNET_SYSERR;
}
TEH_kyc_config.details.oauth2.client_secret = s;
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (TEH_cfg,
"exchange-kyc-oauth2",
"KYC_OAUTH2_POST_URL",
&s))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"exchange-kyc-oauth2",
"KYC_OAUTH2_POST_URL");
return GNUNET_SYSERR;
}
TEH_kyc_config.details.oauth2.post_kyc_redirect_url = s;
return GNUNET_OK;
}
@ -1301,6 +1330,26 @@ exchange_serve_process_config (void)
"CURRENCY");
return GNUNET_SYSERR;
}
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (TEH_cfg,
"exchange",
"BASE_URL",
&TEH_base_url))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"exchange",
"BASE_URL");
return GNUNET_SYSERR;
}
if (! TALER_url_valid_charset (TEH_base_url))
{
GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
"exchange",
"BASE_URL",
"invalid URL");
return GNUNET_SYSERR;
}
if (TEH_KYC_NONE != TEH_kyc_config.mode)
{
if (GNUNET_YES ==
@ -1593,11 +1642,26 @@ do_shutdown (void *cls)
mhd = TALER_MHD_daemon_stop ();
TEH_resume_keys_requests (true);
TEH_reserves_get_cleanup ();
TEH_kyc_proof_cleanup ();
if (NULL != mhd)
MHD_stop_daemon (mhd);
TEH_WIRE_done ();
TEH_keys_finished ();
TALER_EXCHANGEDB_plugin_unload (TEH_plugin);
if (NULL != TEH_plugin)
{
TALER_EXCHANGEDB_plugin_unload (TEH_plugin);
TEH_plugin = NULL;
}
if (NULL != TEH_curl_ctx)
{
GNUNET_CURL_fini (TEH_curl_ctx);
TEH_curl_ctx = NULL;
}
if (NULL != exchange_curl_rc)
{
GNUNET_CURL_gnunet_rc_destroy (exchange_curl_rc);
exchange_curl_rc = NULL;
}
}
@ -1655,6 +1719,17 @@ run (void *cls,
}
TEH_load_terms (TEH_cfg);
TEH_curl_ctx
= GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
&exchange_curl_rc);
if (NULL == TEH_curl_ctx)
{
GNUNET_break (0);
global_ret = EXIT_FAILURE;
GNUNET_SCHEDULER_shutdown ();
return;
}
exchange_curl_rc = GNUNET_CURL_gnunet_rc_create (TEH_curl_ctx);
GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
NULL);
fh = TALER_MHD_bind (TEH_cfg,

View File

@ -105,6 +105,12 @@ struct TEH_KycOptions
*/
char *client_secret;
/**
* Where to redirect clients after the
* Web-based KYC process is done?
*/
char *post_kyc_redirect_url;
} oauth2;
} details;
@ -162,11 +168,20 @@ extern struct TALER_EXCHANGEDB_Plugin *TEH_plugin;
*/
extern char *TEH_currency;
/**
* Our (externally visible) base URL.
*/
extern char *TEH_base_url;
/**
* Are we shutting down?
*/
extern volatile bool MHD_terminating;
/**
* Context for all CURL operations (useful to the event loop)
*/
extern struct GNUNET_CURL_Context *TEH_curl_ctx;
/**
* @brief Struct describing an URL and the handler for it.

View File

@ -381,7 +381,7 @@ TEH_handler_deposit (struct MHD_Connection *connection,
sizeof (deposit));
deposit.coin.coin_pub = *coin_pub;
{
int res;
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (connection,
root,

View File

@ -1345,7 +1345,7 @@ get_date_string (struct GNUNET_TIME_Absolute at,
* @param[in,out] response the response to modify
* @return #GNUNET_OK on success
*/
static int
static enum GNUNET_GenericReturnValue
setup_general_response_headers (const struct TEH_KeyStateHandle *ksh,
struct MHD_Response *response)
{

View File

@ -143,12 +143,31 @@ TEH_handler_kyc_check (
return res;
if (! kcc.kyc.ok)
{
char *url;
char *redirect_uri;
char *redirect_uri_encoded;
GNUNET_assert (TEH_KYC_OAUTH2 == TEH_kyc_config.mode);
return TALER_MHD_REPLY_JSON_PACK (
GNUNET_asprintf (&redirect_uri,
"%s/kyc-proof/%llu",
TEH_base_url,
payment_target_uuid);
redirect_uri_encoded = TALER_urlencode (redirect_uri);
GNUNET_free (redirect_uri);
GNUNET_asprintf (&url,
"%s/login?client_id=%s&redirect_uri=%s",
TEH_kyc_config.details.oauth2.url,
TEH_kyc_config.details.oauth2.client_id,
redirect_uri_encoded);
GNUNET_free (redirect_uri_encoded);
res = TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_ACCEPTED,
GNUNET_JSON_pack_string ("kyc_url",
TEH_kyc_config.details.oauth2.url));
url));
GNUNET_free (url);
return res;
}
{
struct TALER_ExchangePublicKeyP pub;

View File

@ -36,9 +36,99 @@
struct KycProofContext
{
/**
* Kept in a DLL while suspended.
*/
struct KycProofContext *next;
/**
* Kept in a DLL while suspended.
*/
struct KycProofContext *prev;
/**
* Details about the connection we are processing.
*/
struct TEH_RequestContext *rc;
/**
* Handle for the OAuth 2.0 CURL request.
*/
struct GNUNET_CURL_Job *job;
/**
* OAuth 2.0 authorization code.
*/
const char *authorization_code;
/**
* OAuth 2.0 token URL we are using for the
* request.
*/
char *token_url;
/**
* Body of the POST request.
*/
char *post_body;
/**
* Payment target this is about.
*/
unsigned long long payment_target_uuid;
/**
* HTTP response to return.
*/
struct MHD_Response *response;
/**
* HTTP response code to return.
*/
unsigned int response_code;
/**
* #GNUNET_YES if we are suspended,
* #GNUNET_NO if not.
* #GNUNET_SYSERR if we had some error.
*/
enum GNUNET_GenericReturnValue suspended;
};
/**
* Contexts are kept in a DLL while suspended.
*/
static struct KycProofContext *kpc_head;
/**
* Contexts are kept in a DLL while suspended.
*/
static struct KycProofContext *kpc_tail;
void
TEH_kyc_proof_cleanup (void)
{
struct KycProofContext *kpc;
while (NULL != (kpc = kpc_head))
{
if (NULL != kpc->job)
{
GNUNET_CURL_job_cancel (kpc->job);
kpc->job = NULL;
}
GNUNET_CONTAINER_DLL_remove (kpc_head,
kpc_tail,
kpc);
kpc->suspended = GNUNET_NO;
MHD_resume_connection (kpc->rc->connection);
}
}
/**
* Function implementing database transaction to check proof's KYC status.
* Runs the transaction logic; IF it returns a non-error code, the transaction
@ -54,14 +144,185 @@ struct KycProofContext
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
proof_kyc_check (void *cls,
struct MHD_Connection *connection,
MHD_RESULT *mhd_ret)
persist_kyc_ok (void *cls,
struct MHD_Connection *connection,
MHD_RESULT *mhd_ret)
{
struct KycProofContext *kpc = cls;
(void) kpc; // FIXME: do work here!
return -2;
return TEH_plugin->set_kyc_ok (TEH_plugin->cls,
kpc->payment_target_uuid);
}
/**
* After we are done with the CURL interaction we
* need to update our database state.
*
* @param cls our `struct KycProofContext`
* @param response_code HTTP response code from server, 0 on hard error
* @param response in JSON, NULL if response was not in JSON format
*/
static void
handle_curl_login_finished (void *cls,
long response_code,
const void *response)
{
struct KycProofContext *kpc = cls;
const json_t *j = response;
kpc->job = NULL;
switch (response_code)
{
case MHD_HTTP_OK:
{
const char *access_token;
const char *token_type;
uint64_t expires_in_s;
const char *refresh_token;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("access_token",
&access_token),
GNUNET_JSON_spec_string ("token_type",
&token_type),
GNUNET_JSON_spec_uint64 ("expires_in",
&expires_in_s),
GNUNET_JSON_spec_string ("refresh_token",
&refresh_token),
GNUNET_JSON_spec_end ()
};
{
enum GNUNET_GenericReturnValue res;
const char *emsg;
unsigned int line;
res = GNUNET_JSON_parse (j,
spec,
&emsg,
&line);
if (GNUNET_OK != res)
{
GNUNET_break_op (0);
kpc->response
= TALER_MHD_make_error (
TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE,
"Unexpected response from KYC gateway");
kpc->response_code
= MHD_HTTP_BAD_GATEWAY;
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
return;
}
}
if (0 != strcasecmp (token_type,
"bearer"))
{
GNUNET_break_op (0);
kpc->response
= TALER_MHD_make_error (
TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE,
"Unexpected token type in response from KYC gateway");
kpc->response_code
= MHD_HTTP_BAD_GATEWAY;
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
return;
}
/* TODO: Here we might want to keep something to persist in the DB, and
possibly use the access_token to download information we should
persist; then continue! */
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
return;
}
default:
{
const char *msg;
const char *desc;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("error",
&msg),
GNUNET_JSON_spec_string ("error_description",
&desc),
GNUNET_JSON_spec_end ()
};
{
enum GNUNET_GenericReturnValue res;
const char *emsg;
unsigned int line;
res = GNUNET_JSON_parse (j,
spec,
&emsg,
&line);
if (GNUNET_OK != res)
{
GNUNET_break_op (0);
kpc->response
= TALER_MHD_make_error (
TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE,
"Unexpected response from KYC gateway");
kpc->response_code
= MHD_HTTP_BAD_GATEWAY;
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
return;
}
}
/* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED,
we MAY want to in the future look at the requested content type
and possibly respond in JSON if indicated. */
{
char *reply;
GNUNET_asprintf (&reply,
"<html><head><title>%s</title></head><body><h1>%s</h1><p>%s</p></body></html>",
msg,
msg,
desc);
kpc->response
= MHD_create_response_from_buffer (strlen (reply),
reply,
MHD_RESPMEM_MUST_COPY);
GNUNET_assert (NULL != kpc->response);
GNUNET_free (reply);
}
kpc->response_code = MHD_HTTP_FORBIDDEN;
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
}
break;
}
}
/**
* Function called to clean up a context.
*
* @param rc request context
*/
static void
clean_kpc (struct TEH_RequestContext *rc)
{
struct KycProofContext *kpc = rc->rh_ctx;
if (NULL != kpc->job)
{
GNUNET_CURL_job_cancel (kpc->job);
kpc->job = NULL;
}
if (NULL != kpc->response)
{
MHD_destroy_response (kpc->response);
kpc->response = NULL;
}
GNUNET_free (kpc->post_body);
GNUNET_free (kpc->token_url);
GNUNET_free (kpc);
}
@ -70,44 +331,188 @@ TEH_handler_kyc_proof (
struct TEH_RequestContext *rc,
const char *const args[])
{
struct KycProofContext kpc;
MHD_RESULT res;
enum GNUNET_GenericReturnValue ret;
unsigned long long payment_target_uuid;
char dummy;
struct KycProofContext *kpc = rc->rh_ctx;
if (1 !=
sscanf (args[0],
"%llu%c",
&payment_target_uuid,
&dummy))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"payment_target_uuid");
if (NULL == kpc)
{ /* first time */
char dummy;
kpc = GNUNET_new (struct KycProofContext);
kpc->rc = rc;
rc->rh_ctx = kpc;
rc->rh_cleaner = &clean_kpc;
if (1 !=
sscanf (args[0],
"%llu%c",
&kpc->payment_target_uuid,
&dummy))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"payment_target_uuid");
}
kpc->authorization_code
= MHD_lookup_connection_value (rc->connection,
MHD_GET_ARGUMENT_KIND,
"code");
if (NULL == kpc->authorization_code)
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"code");
}
if (TEH_KYC_NONE == TEH_kyc_config.mode)
return TALER_MHD_reply_static (
rc->connection,
MHD_HTTP_NO_CONTENT,
NULL,
NULL,
0);
{
CURL *eh;
eh = curl_easy_init ();
if (NULL == eh)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_ALLOCATION_FAILURE,
"curl_easy_init");
}
GNUNET_asprintf (&kpc->token_url,
"%s/token",
TEH_kyc_config.details.oauth2.url);
GNUNET_assert (CURLE_OK ==
curl_easy_setopt (eh,
CURLOPT_URL,
kpc->token_url));
GNUNET_assert (CURLE_OK ==
curl_easy_setopt (eh,
CURLOPT_POST,
1));
{
char *client_id;
char *redirect_uri;
char *client_secret;
char *authorization_code;
client_id = curl_easy_escape (eh,
TEH_kyc_config.details.oauth2.client_id,
0);
GNUNET_assert (NULL != client_id);
{
char *request_uri;
GNUNET_asprintf (&request_uri,
"%s/login?client_id=%s",
TEH_kyc_config.details.oauth2.url,
TEH_kyc_config.details.oauth2.client_id);
redirect_uri = curl_easy_escape (eh,
request_uri,
0);
GNUNET_free (request_uri);
}
GNUNET_assert (NULL != redirect_uri);
client_secret = curl_easy_escape (eh,
TEH_kyc_config.details.oauth2.
client_secret,
0);
GNUNET_assert (NULL != client_secret);
authorization_code = curl_easy_escape (eh,
kpc->authorization_code,
0);
GNUNET_assert (NULL != authorization_code);
GNUNET_asprintf (&kpc->post_body,
"client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code",
client_id,
redirect_uri,
client_secret,
authorization_code);
curl_free (authorization_code);
curl_free (client_secret);
curl_free (redirect_uri);
curl_free (client_id);
}
GNUNET_assert (CURLE_OK ==
curl_easy_setopt (eh,
CURLOPT_POSTFIELDS,
kpc->post_body));
GNUNET_assert (CURLE_OK ==
curl_easy_setopt (eh,
CURLOPT_FOLLOWLOCATION,
1L));
/* limit MAXREDIRS to 5 as a simple security measure against
a potential infinite loop caused by a malicious target */
GNUNET_assert (CURLE_OK ==
curl_easy_setopt (eh,
CURLOPT_MAXREDIRS,
5L));
kpc->job = GNUNET_CURL_job_add (TEH_curl_ctx,
eh,
&handle_curl_login_finished,
kpc);
kpc->suspended = GNUNET_YES;
GNUNET_CONTAINER_DLL_insert (kpc_head,
kpc_tail,
kpc);
MHD_suspend_connection (rc->connection);
return MHD_YES;
}
}
if (1 || (TEH_KYC_NONE == TEH_kyc_config.mode))
return TALER_MHD_reply_static (
rc->connection,
MHD_HTTP_NO_CONTENT,
NULL,
NULL,
0);
ret = TEH_DB_run_transaction (rc->connection,
"check proof kyc",
&res,
&proof_kyc_check,
&kpc);
if (GNUNET_SYSERR == ret)
if (NULL != kpc->response)
{
/* handle _failed_ resumed cases */
return MHD_queue_response (rc->connection,
kpc->response_code,
kpc->response);
}
/* _successfully_ resumed case */
{
MHD_RESULT res;
enum GNUNET_GenericReturnValue ret;
ret = TEH_DB_run_transaction (kpc->rc->connection,
"check proof kyc",
&res,
&persist_kyc_ok,
kpc);
if (GNUNET_SYSERR == ret)
return res;
}
{
struct MHD_Response *response;
MHD_RESULT res;
response = MHD_create_response_from_buffer (0,
"",
MHD_RESPMEM_PERSISTENT);
if (NULL == response)
{
GNUNET_break (0);
return MHD_NO;
}
GNUNET_break (MHD_YES ==
MHD_add_response_header (
response,
MHD_HTTP_HEADER_LOCATION,
TEH_kyc_config.details.oauth2.post_kyc_redirect_url));
res = MHD_queue_response (rc->connection,
MHD_HTTP_SEE_OTHER,
response);
MHD_destroy_response (response);
return res;
return TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_uint64 ("42",
42));
}
}

View File

@ -25,6 +25,14 @@
#include "taler-exchange-httpd.h"
/**
* Shutdown kyc-proof subsystem. Resumes all suspended long-polling clients
* and cleans up data structures.
*/
void
TEH_kyc_proof_cleanup (void);
/**
* Handle a "/kyc-proof" request.
*

View File

@ -360,6 +360,12 @@ prepare_statements (struct PostgresClosure *pg)
" LIMIT 1;",
1),
#if FIXME_DD23
/* Used in #postgres_set_kyc_ok() */
GNUNET_PQ_make_prepare ("set_kyc_ok",
"UPDATE wire_targets"
" SET kyc_ok=TRUE"
" WHERE wire_target_serial_id=$1",
1),
/* Used in #postgres_get_kyc_status() */
GNUNET_PQ_make_prepare ("get_kyc_status",
"SELECT"
@ -3555,6 +3561,31 @@ postgres_reserves_get (void *cls,
}
/**
* Set the KYC status to "OK" for a bank account.
*
* @param cls the @e cls of this struct with the plugin-specific state
* @param payment_target_uuid which account has been checked
* @param ... possibly additional data to persist (TODO)
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
postgres_set_kyc_ok (void *cls,
uint64_t payment_target_uuid,
...)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_uint64 (&payment_target_uuid),
GNUNET_PQ_query_param_end
};
return GNUNET_PQ_eval_prepared_non_select (pg->conn,
"set_kyc_ok",
params);
}
/**
* Get the KYC status for a bank account.
*
@ -11261,6 +11292,7 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
plugin->iterate_auditor_denominations =
&postgres_iterate_auditor_denominations;
plugin->reserves_get = &postgres_reserves_get;
plugin->set_kyc_ok = &postgres_set_kyc_ok;
plugin->get_kyc_status = &postgres_get_kyc_status;
plugin->select_kyc_status = &postgres_select_kyc_status;
plugin->inselect_wallet_kyc_status = &postgres_inselect_wallet_kyc_status;

View File

@ -59,7 +59,7 @@ struct TALER_CURL_PostContext
* @param body JSON body to add to @e ctx
* @return #GNUNET_OK on success #GNUNET_SYSERR on failure
*/
int
enum GNUNET_GenericReturnValue
TALER_curl_easy_post (struct TALER_CURL_PostContext *ctx,
CURL *eh,
const json_t *body);

View File

@ -2360,6 +2360,20 @@ struct TALER_EXCHANGEDB_Plugin
struct TALER_EXCHANGEDB_KycStatus *kyc);
/**
* Set the KYC status to "OK" for a bank account.
*
* @param cls the @e cls of this struct with the plugin-specific state
* @param payment_target_uuid which account has been checked
* @param ... possibly additional data to persist (TODO)
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
(*set_kyc_ok)(void *cls,
uint64_t payment_target_uuid,
...);
/**
* Get the KYC status for a bank account.
*

View File

@ -82,7 +82,7 @@ struct TALER_EXCHANGE_LinkHandle
* @param[out] pub where to return the public key for the coin
* @return #GNUNET_OK on success, #GNUNET_SYSERR on error
*/
static int
static enum GNUNET_GenericReturnValue
parse_link_coin (const struct TALER_EXCHANGE_LinkHandle *lh,
const json_t *json,
uint32_t coin_num,
@ -175,7 +175,7 @@ parse_link_coin (const struct TALER_EXCHANGE_LinkHandle *lh,
* @param json json reply with the data for one coin
* @return #GNUNET_OK on success, #GNUNET_SYSERR on error
*/
static int
static enum GNUNET_GenericReturnValue
parse_link_ok (struct TALER_EXCHANGE_LinkHandle *lh,
const json_t *json)
{