/* 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 `%s' completed with ec=%d (%s)\n", provider_legitimization_id, 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, redirect_url, 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; char *redirect_url; 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->get_pending_kyc_requirement_process ( TEH_plugin->cls, &h_payto, kyp->section_name, &redirect_url); 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; } if ( (qs > 0) && (NULL != redirect_url) ) { kyp->kyc_url = redirect_url; return qs; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* set up new requirement process */ 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); } /* 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 ( (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; } /* 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 ( (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; } 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 */