/*
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-proof.c
* @brief Handle request for proof for KYC check.
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include
#include
#include "taler_attributes.h"
#include "taler_json_lib.h"
#include "taler_kyclogic_lib.h"
#include "taler_mhd_lib.h"
#include "taler_templating_lib.h"
#include "taler-exchange-httpd_common_kyc.h"
#include "taler-exchange-httpd_kyc-proof.h"
#include "taler-exchange-httpd_responses.h"
/**
* Context for the proof.
*/
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;
/**
* Proof logic to run.
*/
struct TALER_KYCLOGIC_Plugin *logic;
/**
* Configuration for @a logic.
*/
struct TALER_KYCLOGIC_ProviderDetails *pd;
/**
* Asynchronous operation with the proof system.
*/
struct TALER_KYCLOGIC_ProofHandle *ph;
/**
* KYC AML trigger operation.
*/
struct TEH_KycAmlTrigger *kat;
/**
* Process information about the user for the plugin from the database, can
* be NULL.
*/
char *provider_user_id;
/**
* Process information about the legitimization process for the plugin from the
* database, can be NULL.
*/
char *provider_legitimization_id;
/**
* Hash of payment target URI this is about.
*/
struct TALER_PaytoHashP h_payto;
/**
* HTTP response to return.
*/
struct MHD_Response *response;
/**
* Provider configuration section name of the logic we are running.
*/
const char *provider_section;
/**
* Row in the database for this legitimization operation.
*/
uint64_t process_row;
/**
* HTTP response code to return.
*/
unsigned int response_code;
/**
* True if we are suspended,
*/
bool 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;
/**
* Resume processing the @a kpc request.
*
* @param kpc request to resume
*/
static void
kpc_resume (struct KycProofContext *kpc)
{
GNUNET_assert (GNUNET_YES == kpc->suspended);
kpc->suspended = false;
GNUNET_CONTAINER_DLL_remove (kpc_head,
kpc_tail,
kpc);
MHD_resume_connection (kpc->rc->connection);
TALER_MHD_daemon_trigger ();
}
void
TEH_kyc_proof_cleanup (void)
{
struct KycProofContext *kpc;
while (NULL != (kpc = kpc_head))
{
if (NULL != kpc->ph)
{
kpc->logic->proof_cancel (kpc->ph);
kpc->ph = NULL;
}
kpc_resume (kpc);
}
}
/**
* Function called after the KYC-AML trigger is done.
*
* @param cls closure
* @param http_status final HTTP status to return
* @param[in] response final HTTP ro return
*/
static void
proof_finish (
void *cls,
unsigned int http_status,
struct MHD_Response *response)
{
struct KycProofContext *kpc = cls;
kpc->kat = NULL;
kpc->response_code = http_status;
kpc->response = response;
kpc_resume (kpc);
}
/**
* Generate HTML error for @a connection using @a template.
*
* @param connection HTTP client connection
* @param template template to expand
* @param[in,out] http_status HTTP status of the response
* @param ec Taler error code to return
* @param message extended message to return
* @return MHD response object
*/
struct MHD_Response *
make_html_error (struct MHD_Connection *connection,
const char *template,
unsigned int *http_status,
enum TALER_ErrorCode ec,
const char *message)
{
struct MHD_Response *response = NULL;
json_t *body;
body = GNUNET_JSON_PACK (
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("message",
message)),
TALER_JSON_pack_ec (
ec));
GNUNET_break (
GNUNET_SYSERR !=
TALER_TEMPLATING_build (connection,
http_status,
template,
NULL,
NULL,
body,
&response));
json_decref (body);
return response;
}
/**
* Respond with an HTML message on the given @a rc.
*
* @param[in,out] rc request to respond to
* @param http_status HTTP status code to use
* @param template template to fill in
* @param ec error code to use for the template
* @param message additional message to return
* @return MHD result code
*/
static MHD_RESULT
respond_html_ec (struct TEH_RequestContext *rc,
unsigned int http_status,
const char *template,
enum TALER_ErrorCode ec,
const char *message)
{
struct MHD_Response *response;
MHD_RESULT res;
response = make_html_error (rc->connection,
template,
&http_status,
ec,
message);
res = MHD_queue_response (rc->connection,
http_status,
response);
MHD_destroy_response (response);
return res;
}
/**
* Function called with the result of a proof check operation.
*
* Note that the "decref" for the @a response
* will be done by the callee and MUST NOT be done by the plugin.
*
* @param cls closure
* @param status KYC status
* @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 expiration until when is the KYC check valid
* @param attributes user attributes returned by the provider
* @param http_status HTTP status code of @a response
* @param[in] response to return to the HTTP client
*/
static void
proof_cb (
void *cls,
enum TALER_KYCLOGIC_KycStatus status,
const char *provider_user_id,
const char *provider_legitimization_id,
struct GNUNET_TIME_Absolute expiration,
const json_t *attributes,
unsigned int http_status,
struct MHD_Response *response)
{
struct KycProofContext *kpc = cls;
struct TEH_RequestContext *rc = kpc->rc;
struct GNUNET_AsyncScopeSave old_scope;
kpc->ph = NULL;
GNUNET_async_scope_enter (&rc->async_scope_id,
&old_scope);
switch (status)
{
case TALER_KYCLOGIC_STATUS_SUCCESS:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC process #%llu succeeded with KYC provider\n",
(unsigned long long) kpc->process_row);
kpc->kat = TEH_kyc_finished (&rc->async_scope_id,
kpc->process_row,
&kpc->h_payto,
kpc->provider_section,
provider_user_id,
provider_legitimization_id,
expiration,
attributes,
http_status,
response,
&proof_finish,
kpc);
if (NULL == kpc->kat)
{
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
if (NULL != response)
MHD_destroy_response (response);
response = make_html_error (kpc->rc->connection,
"kyc-proof-internal-error",
&http_status,
TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION,
"[exchange] AML_KYC_TRIGGER");
}
break;
case TALER_KYCLOGIC_STATUS_FAILED:
case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED:
case TALER_KYCLOGIC_STATUS_USER_ABORTED:
case TALER_KYCLOGIC_STATUS_ABORTED:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC process %s/%s (Row #%llu) failed: %d\n",
provider_user_id,
provider_legitimization_id,
(unsigned long long) kpc->process_row,
status);
if (5 == http_status / 100)
{
char *msg;
/* OAuth2 server had a problem, do NOT log this as a KYC failure */
if (NULL != response)
MHD_destroy_response (response);
GNUNET_asprintf (&msg,
"Failure by KYC provider (HTTP status %u)\n",
http_status);
http_status = MHD_HTTP_BAD_GATEWAY;
response = make_html_error (kpc->rc->connection,
"kyc-proof-internal-error",
&http_status,
TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY,
msg);
GNUNET_free (msg);
}
else
{
if (! TEH_kyc_failed (kpc->process_row,
&kpc->h_payto,
kpc->provider_section,
provider_user_id,
provider_legitimization_id))
{
GNUNET_break (0);
if (NULL != response)
MHD_destroy_response (response);
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
response = make_html_error (kpc->rc->connection,
"kyc-proof-internal-error",
&http_status,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert_kyc_failure");
}
}
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC status of %s/%s (Row #%llu) is %d\n",
provider_user_id,
provider_legitimization_id,
(unsigned long long) kpc->process_row,
(int) status);
break;
}
if (NULL == kpc->kat)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"KYC process #%llu failed with status %d\n",
(unsigned long long) kpc->process_row,
status);
proof_finish (kpc,
http_status,
response);
}
GNUNET_async_scope_restore (&old_scope);
}
/**
* 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->ph)
{
kpc->logic->proof_cancel (kpc->ph);
kpc->ph = NULL;
}
if (NULL != kpc->kat)
{
TEH_kyc_finished_cancel (kpc->kat);
kpc->kat = NULL;
}
if (NULL != kpc->response)
{
MHD_destroy_response (kpc->response);
kpc->response = NULL;
}
GNUNET_free (kpc->provider_user_id);
GNUNET_free (kpc->provider_legitimization_id);
GNUNET_free (kpc);
}
MHD_RESULT
TEH_handler_kyc_proof (
struct TEH_RequestContext *rc,
const char *const args[1])
{
struct KycProofContext *kpc = rc->rh_ctx;
const char *provider_section_or_logic = args[0];
if (NULL == kpc)
{
/* first time */
if (NULL == provider_section_or_logic)
{
GNUNET_break_op (0);
return respond_html_ec (rc,
MHD_HTTP_NOT_FOUND,
"kyc-proof-endpoint-unknown",
TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
"'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required");
}
kpc = GNUNET_new (struct KycProofContext);
kpc->rc = rc;
rc->rh_ctx = kpc;
rc->rh_cleaner = &clean_kpc;
TALER_MHD_parse_request_arg_auto_t (rc->connection,
"state",
&kpc->h_payto);
if (GNUNET_OK !=
TALER_KYCLOGIC_lookup_logic (provider_section_or_logic,
&kpc->logic,
&kpc->pd,
&kpc->provider_section))
{
GNUNET_break_op (0);
return respond_html_ec (rc,
MHD_HTTP_NOT_FOUND,
"kyc-proof-target-unknown",
TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN,
provider_section_or_logic);
}
if (NULL != kpc->provider_section)
{
enum GNUNET_DB_QueryStatus qs;
struct GNUNET_TIME_Absolute expiration;
if (0 != strcmp (provider_section_or_logic,
kpc->provider_section))
{
GNUNET_break_op (0);
return respond_html_ec (rc,
MHD_HTTP_BAD_REQUEST,
"kyc-proof-bad-request",
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"PROVIDER_SECTION");
}
qs = TEH_plugin->lookup_kyc_process_by_account (
TEH_plugin->cls,
kpc->provider_section,
&kpc->h_payto,
&kpc->process_row,
&expiration,
&kpc->provider_user_id,
&kpc->provider_legitimization_id);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
case GNUNET_DB_STATUS_SOFT_ERROR:
return respond_html_ec (rc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"kyc-proof-internal-error",
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_kyc_process_by_account");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
return respond_html_ec (rc,
MHD_HTTP_NOT_FOUND,
"kyc-proof-target-unknown",
TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN,
kpc->provider_section);
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
break;
}
if (GNUNET_TIME_absolute_is_future (expiration))
{
/* KYC not required */
return respond_html_ec (rc,
MHD_HTTP_OK,
"kyc-proof-already-done",
TALER_EC_NONE,
NULL);
}
}
kpc->ph = kpc->logic->proof (kpc->logic->cls,
kpc->pd,
rc->connection,
&kpc->h_payto,
kpc->process_row,
kpc->provider_user_id,
kpc->provider_legitimization_id,
&proof_cb,
kpc);
if (NULL == kpc->ph)
{
GNUNET_break (0);
return respond_html_ec (rc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"kyc-proof-internal-error",
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
"could not start proof with KYC logic");
}
kpc->suspended = true;
GNUNET_CONTAINER_DLL_insert (kpc_head,
kpc_tail,
kpc);
MHD_suspend_connection (rc->connection);
return MHD_YES;
}
if (NULL == kpc->response)
{
GNUNET_break (0);
return respond_html_ec (rc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"kyc-proof-internal-error",
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
"handler resumed without response");
}
/* return response from KYC logic */
return MHD_queue_response (rc->connection,
kpc->response_code,
kpc->response);
}
/* end of taler-exchange-httpd_kyc-proof.c */