/*
This file is part of TALER
Copyright (C) 2021-2023 Taler Systems SA
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, see
*/
/**
* @file taler-exchange-httpd_kyc-check.c
* @brief Handle request for generic KYC check.
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include
#include
#include
#include "taler_json_lib.h"
#include "taler_kyclogic_lib.h"
#include "taler_mhd_lib.h"
#include "taler_signatures.h"
#include "taler_dbevents.h"
#include "taler-exchange-httpd_keys.h"
#include "taler-exchange-httpd_kyc-wallet.h"
#include "taler-exchange-httpd_responses.h"
/**
* Reserve GET request that is long-polling.
*/
struct KycPoller
{
/**
* Kept in a DLL.
*/
struct KycPoller *next;
/**
* Kept in a DLL.
*/
struct KycPoller *prev;
/**
* Connection we are handling.
*/
struct MHD_Connection *connection;
/**
* Logic for @e ih
*/
struct TALER_KYCLOGIC_Plugin *ih_logic;
/**
* Handle to asynchronously running KYC initiation
* request.
*/
struct TALER_KYCLOGIC_InitiateHandle *ih;
/**
* Subscription for the database event we are
* waiting for.
*/
struct GNUNET_DB_EventHandler *eh;
/**
* Row of the requirement being checked.
*/
uint64_t requirement_row;
/**
* Row of KYC process being initiated.
*/
uint64_t process_row;
/**
* Hash of the payto:// URI we are confirming to
* have finished the KYC for.
*/
struct TALER_PaytoHashP h_payto;
/**
* When will this request time out?
*/
struct GNUNET_TIME_Absolute timeout;
/**
* If the KYC complete, what kind of data was collected?
*/
json_t *kyc_details;
/**
* Set to starting URL of KYC process if KYC is required.
*/
char *kyc_url;
/**
* Set to error details, on error (@ec not TALER_EC_NONE).
*/
char *hint;
/**
* Name of the section of the provider in the configuration.
*/
const char *section_name;
/**
* Set to AML status of the account.
*/
enum TALER_AmlDecisionState aml_status;
/**
* Set to error encountered with KYC logic, if any.
*/
enum TALER_ErrorCode ec;
/**
* What kind of entity is doing the KYC check?
*/
enum TALER_KYCLOGIC_KycUserType ut;
/**
* True if we are still suspended.
*/
bool suspended;
/**
* False if KYC is not required.
*/
bool kyc_required;
/**
* True if we once tried the KYC initiation.
*/
bool ih_done;
};
/**
* Head of list of requests in long polling.
*/
static struct KycPoller *kyp_head;
/**
* Tail of list of requests in long polling.
*/
static struct KycPoller *kyp_tail;
void
TEH_kyc_check_cleanup ()
{
struct KycPoller *kyp;
while (NULL != (kyp = kyp_head))
{
GNUNET_CONTAINER_DLL_remove (kyp_head,
kyp_tail,
kyp);
if (NULL != kyp->ih)
{
kyp->ih_logic->initiate_cancel (kyp->ih);
kyp->ih = NULL;
}
if (kyp->suspended)
{
kyp->suspended = false;
MHD_resume_connection (kyp->connection);
}
}
}
/**
* Function called once a connection is done to
* clean up the `struct ReservePoller` state.
*
* @param rc context to clean up for
*/
static void
kyp_cleanup (struct TEH_RequestContext *rc)
{
struct KycPoller *kyp = rc->rh_ctx;
GNUNET_assert (! kyp->suspended);
if (NULL != kyp->eh)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Cancelling DB event listening\n");
TEH_plugin->event_listen_cancel (TEH_plugin->cls,
kyp->eh);
kyp->eh = NULL;
}
if (NULL != kyp->ih)
{
kyp->ih_logic->initiate_cancel (kyp->ih);
kyp->ih = NULL;
}
json_decref (kyp->kyc_details);
GNUNET_free (kyp->kyc_url);
GNUNET_free (kyp->hint);
GNUNET_free (kyp);
}
/**
* Function called with the result of a KYC initiation
* operation.
*
* @param cls closure with our `struct KycPoller *`
* @param ec #TALER_EC_NONE on success
* @param redirect_url set to where to redirect the user on success, NULL on failure
* @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown
* @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown
* @param error_msg_hint set to additional details to return to user, NULL on success
*/
static void
initiate_cb (
void *cls,
enum TALER_ErrorCode ec,
const char *redirect_url,
const char *provider_user_id,
const char *provider_legitimization_id,
const char *error_msg_hint)
{
struct KycPoller *kyp = cls;
enum GNUNET_DB_QueryStatus qs;
kyp->ih = NULL;
kyp->ih_done = true;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC initiation completed with ec=%d (%s)\n",
ec,
(TALER_EC_NONE == ec)
? redirect_url
: error_msg_hint);
kyp->ec = ec;
if (TALER_EC_NONE == ec)
{
kyp->kyc_url = GNUNET_strdup (redirect_url);
}
else
{
kyp->hint = GNUNET_strdup (error_msg_hint);
}
qs = TEH_plugin->update_kyc_process_by_row (
TEH_plugin->cls,
kyp->process_row,
kyp->section_name,
&kyp->h_payto,
provider_user_id,
provider_legitimization_id,
GNUNET_TIME_UNIT_ZERO_ABS);
if (qs < 0)
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"KYC requirement update failed for %s with status %d at %s:%u\n",
TALER_B2S (&kyp->h_payto),
qs,
__FILE__,
__LINE__);
GNUNET_assert (kyp->suspended);
kyp->suspended = false;
GNUNET_CONTAINER_DLL_remove (kyp_head,
kyp_tail,
kyp);
MHD_resume_connection (kyp->connection);
TALER_MHD_daemon_trigger ();
}
/**
* Function implementing database transaction to check wallet's KYC status.
* Runs the transaction logic; IF it returns a non-error code, the transaction
* logic MUST NOT queue a MHD response. IF it returns an hard error, the
* transaction logic MUST queue a MHD response and set @a mhd_ret. IF it
* returns the soft error code, the function MAY be called again to retry and
* MUST not queue a MHD response.
*
* @param cls closure with a `struct KycPoller *`
* @param connection MHD request which triggered the transaction
* @param[out] mhd_ret set to MHD response status for @a connection,
* if transaction failed (!)
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
kyc_check (void *cls,
struct MHD_Connection *connection,
MHD_RESULT *mhd_ret)
{
struct KycPoller *kyp = cls;
enum GNUNET_DB_QueryStatus qs;
struct TALER_KYCLOGIC_ProviderDetails *pd;
enum GNUNET_GenericReturnValue ret;
struct TALER_PaytoHashP h_payto;
char *requirements;
bool satisfied;
qs = TEH_plugin->lookup_kyc_requirement_by_row (
TEH_plugin->cls,
kyp->requirement_row,
&requirements,
&kyp->aml_status,
&h_payto);
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"No KYC requirements open for %llu\n",
(unsigned long long) kyp->requirement_row);
return qs;
}
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs);
return qs;
}
if (0 !=
GNUNET_memcmp (&kyp->h_payto,
&h_payto))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Requirement %llu provided, but h_payto does not match\n",
(unsigned long long) kyp->requirement_row);
GNUNET_break_op (0);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_FORBIDDEN,
TALER_EC_EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED,
"h_payto");
GNUNET_free (requirements);
return GNUNET_DB_STATUS_HARD_ERROR;
}
qs = TALER_KYCLOGIC_check_satisfied (
&requirements,
&h_payto,
&kyp->kyc_details,
TEH_plugin->select_satisfied_kyc_processes,
TEH_plugin->cls,
&satisfied);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
GNUNET_break (0);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"kyc_test_required");
GNUNET_free (requirements);
return GNUNET_DB_STATUS_HARD_ERROR;
}
if (satisfied)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC requirements `%s' already satisfied\n",
requirements);
GNUNET_free (requirements);
return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
}
kyp->kyc_required = true;
ret = TALER_KYCLOGIC_requirements_to_logic (requirements,
kyp->ut,
&kyp->ih_logic,
&pd,
&kyp->section_name);
if (GNUNET_OK != ret)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"KYC requirements `%s' cannot be checked, but are set as required in database!\n",
requirements);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_GONE,
requirements);
GNUNET_free (requirements);
return GNUNET_DB_STATUS_HARD_ERROR;
}
GNUNET_free (requirements);
if (kyp->ih_done)
return qs;
qs = TEH_plugin->insert_kyc_requirement_process (
TEH_plugin->cls,
&h_payto,
kyp->section_name,
NULL,
NULL,
&kyp->process_row);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
GNUNET_break (0);
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert_kyc_requirement_process");
return GNUNET_DB_STATUS_HARD_ERROR;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Initiating KYC check with logic %s\n",
kyp->ih_logic->name);
kyp->ih = kyp->ih_logic->initiate (kyp->ih_logic->cls,
pd,
&h_payto,
kyp->process_row,
&initiate_cb,
kyp);
GNUNET_break (NULL != kyp->ih);
return qs;
}
/**
* Function called on events received from Postgres.
* Wakes up long pollers.
*
* @param cls the `struct TEH_RequestContext *`
* @param extra additional event data provided
* @param extra_size number of bytes in @a extra
*/
static void
db_event_cb (void *cls,
const void *extra,
size_t extra_size)
{
struct TEH_RequestContext *rc = cls;
struct KycPoller *kyp = rc->rh_ctx;
struct GNUNET_AsyncScopeSave old_scope;
(void) extra;
(void) extra_size;
if (! kyp->suspended)
return; /* event triggered while main transaction
was still running, or got multiple wake-up events */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Received KYC update event\n");
kyp->suspended = false;
GNUNET_async_scope_enter (&rc->async_scope_id,
&old_scope);
TEH_check_invariants ();
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Resuming from long-polling on KYC status\n");
GNUNET_CONTAINER_DLL_remove (kyp_head,
kyp_tail,
kyp);
MHD_resume_connection (kyp->connection);
TALER_MHD_daemon_trigger ();
TEH_check_invariants ();
GNUNET_async_scope_restore (&old_scope);
}
MHD_RESULT
TEH_handler_kyc_check (
struct TEH_RequestContext *rc,
const char *const args[3])
{
struct KycPoller *kyp = rc->rh_ctx;
MHD_RESULT res;
enum GNUNET_GenericReturnValue ret;
struct GNUNET_TIME_Timestamp now;
if (NULL == kyp)
{
kyp = GNUNET_new (struct KycPoller);
kyp->connection = rc->connection;
rc->rh_ctx = kyp;
rc->rh_cleaner = &kyp_cleanup;
{
unsigned long long requirement_row;
char dummy;
if (1 !=
sscanf (args[0],
"%llu%c",
&requirement_row,
&dummy))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"requirement_row");
}
kyp->requirement_row = (uint64_t) requirement_row;
}
if (GNUNET_OK !=
GNUNET_STRINGS_string_to_data (args[1],
strlen (args[1]),
&kyp->h_payto,
sizeof (kyp->h_payto)))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"h_payto");
}
if (GNUNET_OK !=
TALER_KYCLOGIC_kyc_user_type_from_string (args[2],
&kyp->ut))
{
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (rc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"usertype");
}
TALER_MHD_parse_request_timeout (rc->connection,
&kyp->timeout);
}
if ( (NULL == kyp->eh) &&
GNUNET_TIME_absolute_is_future (kyp->timeout) )
{
struct TALER_KycCompletedEventP rep = {
.header.size = htons (sizeof (rep)),
.header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED),
.h_payto = kyp->h_payto
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Starting DB event listening\n");
kyp->eh = TEH_plugin->event_listen (
TEH_plugin->cls,
GNUNET_TIME_absolute_get_remaining (kyp->timeout),
&rep.header,
&db_event_cb,
rc);
}
now = GNUNET_TIME_timestamp_get ();
ret = TEH_DB_run_transaction (rc->connection,
"kyc check",
TEH_MT_REQUEST_OTHER,
&res,
&kyc_check,
kyp);
if (GNUNET_SYSERR == ret)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Transaction failed.\n");
return res;
}
if ( (NULL == kyp->ih) &&
(! kyp->kyc_required) )
{
if (TALER_AML_NORMAL != kyp->aml_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC is OK, but AML active: %d\n",
(int) kyp->aml_status);
return TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
GNUNET_JSON_pack_uint64 ("aml_status",
kyp->aml_status));
}
/* KYC not required */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC not required %llu\n",
(unsigned long long) kyp->requirement_row);
return TALER_MHD_reply_static (
rc->connection,
MHD_HTTP_NO_CONTENT,
NULL,
NULL,
0);
}
if (NULL != kyp->ih)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Suspending HTTP request on KYC logic...\n");
kyp->suspended = true;
GNUNET_CONTAINER_DLL_insert (kyp_head,
kyp_tail,
kyp);
MHD_suspend_connection (kyp->connection);
return MHD_YES;
}
/* long polling? */
if ( (NULL != kyp->section_name) &&
GNUNET_TIME_absolute_is_future (kyp->timeout))
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Suspending HTTP request on timeout (%s) now...\n",
GNUNET_TIME_relative2s (GNUNET_TIME_absolute_get_remaining (
kyp->timeout),
true));
GNUNET_assert (NULL != kyp->eh);
kyp->suspended = true;
kyp->section_name = NULL;
GNUNET_CONTAINER_DLL_insert (kyp_head,
kyp_tail,
kyp);
MHD_suspend_connection (kyp->connection);
return MHD_YES;
}
/* KYC plugin generated reply? */
if (NULL != kyp->kyc_url)
{
return TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_ACCEPTED,
GNUNET_JSON_pack_uint64 ("aml_status",
kyp->aml_status),
GNUNET_JSON_pack_string ("kyc_url",
kyp->kyc_url));
}
if (TALER_EC_NONE != kyp->ec)
{
return TALER_MHD_reply_with_ec (rc->connection,
kyp->ec,
kyp->hint);
}
/* KYC must have succeeded! */
{
struct TALER_ExchangePublicKeyP pub;
struct TALER_ExchangeSignatureP sig;
enum TALER_ErrorCode ec;
if (TALER_EC_NONE !=
(ec = TALER_exchange_online_account_setup_success_sign (
&TEH_keys_exchange_sign_,
&kyp->h_payto,
kyp->kyc_details,
now,
&pub,
&sig)))
{
return TALER_MHD_reply_with_ec (rc->connection,
ec,
NULL);
}
return TALER_MHD_REPLY_JSON_PACK (
rc->connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_data_auto ("exchange_sig",
&sig),
GNUNET_JSON_pack_data_auto ("exchange_pub",
&pub),
GNUNET_JSON_pack_uint64 ("aml_status",
kyp->aml_status),
GNUNET_JSON_pack_object_incref ("kyc_details",
kyp->kyc_details),
GNUNET_JSON_pack_timestamp ("now",
now));
}
}
/* end of taler-exchange-httpd_kyc-check.c */