/* This file is part of TALER Copyright (C) 2022-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-webhook.c * @brief Handle notification of KYC completion via webhook. * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include "taler_attributes.h" #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler_kyclogic_lib.h" #include "taler-exchange-httpd_common_kyc.h" #include "taler-exchange-httpd_kyc-webhook.h" #include "taler-exchange-httpd_responses.h" /** * Context for the webhook. */ struct KycWebhookContext { /** * Kept in a DLL while suspended. */ struct KycWebhookContext *next; /** * Kept in a DLL while suspended. */ struct KycWebhookContext *prev; /** * Details about the connection we are processing. */ struct TEH_RequestContext *rc; /** * Handle for the KYC-AML trigger interaction. */ struct TEH_KycAmlTrigger *kat; /** * Plugin responsible for the webhook. */ struct TALER_KYCLOGIC_Plugin *plugin; /** * Section in the configuration of the configured * KYC provider. */ const char *provider_section; /** * Configuration for the specific action. */ struct TALER_KYCLOGIC_ProviderDetails *pd; /** * Webhook activity. */ struct TALER_KYCLOGIC_WebhookHandle *wh; /** * 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 KycWebhookContext *kwh_head; /** * Contexts are kept in a DLL while suspended. */ static struct KycWebhookContext *kwh_tail; /** * Resume processing the @a kwh request. * * @param kwh request to resume */ static void kwh_resume (struct KycWebhookContext *kwh) { GNUNET_assert (GNUNET_YES == kwh->suspended); kwh->suspended = GNUNET_NO; GNUNET_CONTAINER_DLL_remove (kwh_head, kwh_tail, kwh); MHD_resume_connection (kwh->rc->connection); TALER_MHD_daemon_trigger (); } void TEH_kyc_webhook_cleanup (void) { struct KycWebhookContext *kwh; while (NULL != (kwh = kwh_head)) { if (NULL != kwh->wh) { kwh->plugin->webhook_cancel (kwh->wh); kwh->wh = NULL; } kwh_resume (kwh); } } /** * Function called after the KYC-AML trigger is done. * * @param cls closure with a `struct KycWebhookContext *` * @param http_status final HTTP status to return * @param[in] response final HTTP ro return */ static void kyc_aml_webhook_finished ( void *cls, unsigned int http_status, struct MHD_Response *response) { struct KycWebhookContext *kwh = cls; kwh->kat = NULL; kwh->response = response; kwh->response_code = http_status; kwh_resume (kwh); } /** * Function called with the result of a KYC webhook operation. * * Note that the "decref" for the @a response * will be done by the plugin. * * @param cls closure * @param process_row legitimization process the webhook was about * @param account_id account the webhook was about * @param provider_section name of the configuration section of the logic that was run * @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 status KYC status * @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 webhook_finished_cb ( void *cls, uint64_t process_row, const struct TALER_PaytoHashP *account_id, const char *provider_section, const char *provider_user_id, const char *provider_legitimization_id, enum TALER_KYCLOGIC_KycStatus status, struct GNUNET_TIME_Absolute expiration, const json_t *attributes, unsigned int http_status, struct MHD_Response *response) { struct KycWebhookContext *kwh = cls; kwh->wh = NULL; switch (status) { case TALER_KYCLOGIC_STATUS_SUCCESS: kwh->kat = TEH_kyc_finished ( &kwh->rc->async_scope_id, process_row, account_id, provider_section, provider_user_id, provider_legitimization_id, expiration, attributes, http_status, response, &kyc_aml_webhook_finished, kwh); if (NULL == kwh->kat) { if (NULL != response) MHD_destroy_response (response); http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; response = TALER_MHD_make_error ( TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, "[exchange] AML_KYC_TRIGGER"); break; } return; 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) process_row, status); if (! TEH_kyc_failed (process_row, account_id, 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 = TALER_MHD_make_error ( 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) process_row, (int) status); break; } GNUNET_break (NULL == kwh->kat); kyc_aml_webhook_finished (kwh, http_status, response); } /** * Function called to clean up a context. * * @param rc request context */ static void clean_kwh (struct TEH_RequestContext *rc) { struct KycWebhookContext *kwh = rc->rh_ctx; if (NULL != kwh->wh) { kwh->plugin->webhook_cancel (kwh->wh); kwh->wh = NULL; } if (NULL != kwh->kat) { TEH_kyc_finished_cancel (kwh->kat); kwh->kat = NULL; } if (NULL != kwh->response) { MHD_destroy_response (kwh->response); kwh->response = NULL; } GNUNET_free (kwh); } /** * Handle a (GET or POST) "/kyc-webhook" request. * * @param rc request to handle * @param method HTTP request method used by the client * @param root uploaded JSON body (can be NULL) * @param args one argument with the legitimization_uuid * @return MHD result code */ static MHD_RESULT handler_kyc_webhook_generic ( struct TEH_RequestContext *rc, const char *method, const json_t *root, const char *const args[]) { struct KycWebhookContext *kwh = rc->rh_ctx; if (NULL == kwh) { /* first time */ kwh = GNUNET_new (struct KycWebhookContext); kwh->rc = rc; rc->rh_ctx = kwh; rc->rh_cleaner = &clean_kwh; if ( (NULL == args[0]) || (GNUNET_OK != TALER_KYCLOGIC_lookup_logic (args[0], &kwh->plugin, &kwh->pd, &kwh->provider_section)) ) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "KYC logic `%s' unknown (check KYC provider configuration)\n", args[0]); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, args[0]); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC logic `%s' mapped to section %s\n", args[0], kwh->provider_section); kwh->wh = kwh->plugin->webhook (kwh->plugin->cls, kwh->pd, TEH_plugin->kyc_provider_account_lookup, TEH_plugin->cls, method, &args[1], rc->connection, root, &webhook_finished_cb, kwh); if (NULL == kwh->wh) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "failed to run webhook logic"); } kwh->suspended = GNUNET_YES; GNUNET_CONTAINER_DLL_insert (kwh_head, kwh_tail, kwh); MHD_suspend_connection (rc->connection); return MHD_YES; } GNUNET_break (GNUNET_NO == kwh->suspended); if (NULL != kwh->response) { MHD_RESULT res; res = MHD_queue_response (rc->connection, kwh->response_code, kwh->response); GNUNET_break (MHD_YES == res); return res; } /* We resumed, but got no response? This should not happen. */ GNUNET_break (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "resumed without response"); } MHD_RESULT TEH_handler_kyc_webhook_get ( struct TEH_RequestContext *rc, const char *const args[]) { return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_GET, NULL, args); } MHD_RESULT TEH_handler_kyc_webhook_post ( struct TEH_RequestContext *rc, const json_t *root, const char *const args[]) { return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_POST, root, args); } /* end of taler-exchange-httpd_kyc-webhook.c */