/* This file is part of GNU Taler (C) 2021 Taler Systems SA GNU 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. GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_private-get-instances-ID-kyc.c * @brief implementing GET /instances/$ID/kyc request handling * @author Christian Grothoff */ #include "platform.h" #include "taler-merchant-httpd_private-get-instances-ID-kyc.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_exchanges.h" #include /** * We do not re-check an acceptable KYC status for * a month, as usually a KYC never expires. */ #define STALE_KYC_TIMEOUT GNUNET_TIME_UNIT_MONTHS /** * How long should clients cache a KYC failure response? */ #define EXPIRATION_KYC_FAILURE GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MINUTES, 5) /** * How long should clients cache a KYC success response? */ #define EXPIRATION_KYC_SUCCESS GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_HOURS, 1) /** * Information we keep per /kyc request. */ struct KycContext; /** * Structure for tracking requests to the exchange's * ``/kyc-check`` API. */ struct ExchangeKycRequest { /** * Kept in a DLL. */ struct ExchangeKycRequest *next; /** * Kept in a DLL. */ struct ExchangeKycRequest *prev; /** * Find operation where we connect to the respective exchange. */ struct TMH_EXCHANGES_FindOperation *fo; /** * KYC request this exchange request is made for. */ struct KycContext *kc; /** * Hash of the wire account (with salt) we are checking. */ struct TALER_MerchantWireHashP h_wire; /** * Handle for the actual HTTP request to the exchange. */ struct TALER_EXCHANGE_KycCheckHandle *kyc; /** * KYC number used by the exchange. */ uint64_t exchange_kyc_serial; /** * Our account's payto URI. */ char *payto_uri; /** * Base URL of the exchange. */ char *exchange_url; /** * Timestamp when we last got a reply from the exchange. */ struct GNUNET_TIME_Timestamp last_check; /** * Last KYC status returned by the exchange. */ bool kyc_ok; }; /** * Information we keep per /kyc request. */ struct KycContext { /** * Stored in a DLL. */ struct KycContext *next; /** * Stored in a DLL. */ struct KycContext *prev; /** * Connection we are handling. */ struct MHD_Connection *connection; /** * Instance we are serving. */ struct TMH_MerchantInstance *mi; /** * Our handler context. */ struct TMH_HandlerContext *hc; /** * Task to trigger on request timeout, or NULL. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * Response to return, NULL if we don't have one yet. */ struct MHD_Response *response; /** * JSON array where we are building up the array with * pending KYC operations. */ json_t *pending_kycs; /** * JSON array where we are building up the array with * troubled KYC operations. */ json_t *timeout_kycs; /** * Head of DLL of requests we are making to an * exchange to inquire about the latest KYC status. */ struct ExchangeKycRequest *exchange_pending_head; /** * Tail of DLL of requests we are making to an * exchange to inquire about the latest KYC status. */ struct ExchangeKycRequest *exchange_pending_tail; /** * Set to the exchange URL, or NULL to not filter by * exchange. */ const char *exchange_url; /** * Set to the h_wire of the merchant account if * @a have_h_wire is true, used to filter by account. */ struct TALER_MerchantWireHashP h_wire; /** * How long are we willing to wait for the exchange(s)? */ struct GNUNET_TIME_Relative timeout; /** * HTTP status code to use for the reply, i.e 200 for "OK". * Special value UINT_MAX is used to indicate hard errors * (no reply, return #MHD_NO). */ unsigned int response_code; /** * #GNUNET_NO if the @e connection was not suspended, * #GNUNET_YES if the @e connection was suspended, * #GNUNET_SYSERR if @e connection was resumed to as * part of #MH_force_pc_resume during shutdown. */ enum GNUNET_GenericReturnValue suspended; /** * True if @e h_wire was given. */ bool have_h_wire; /** * We're still waiting on the exchange to determine * the KYC status of our deposit(s). */ bool kyc_serial_pending; }; /** * Head of DLL. */ static struct KycContext *kc_head; /** * Tail of DLL. */ static struct KycContext *kc_tail; void TMH_force_kyc_resume () { for (struct KycContext *kc = kc_head; NULL != kc; kc = kc->next) { if (NULL != kc->timeout_task) { GNUNET_SCHEDULER_cancel (kc->timeout_task); kc->timeout_task = NULL; } if (GNUNET_YES == kc->suspended) { kc->suspended = GNUNET_SYSERR; MHD_resume_connection (kc->connection); } } } /** * Custom cleanup routine for a `struct KycContext`. * * @param cls the `struct KycContext` to clean up. */ static void kyc_context_cleanup (void *cls) { struct KycContext *kc = cls; struct ExchangeKycRequest *ekr; while (NULL != (ekr = kc->exchange_pending_head)) { GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, kc->exchange_pending_tail, ekr); if (NULL != ekr->kyc) { TALER_EXCHANGE_kyc_check_cancel (ekr->kyc); ekr->kyc = NULL; } if (NULL != ekr->fo) { TMH_EXCHANGES_find_exchange_cancel (ekr->fo); ekr->fo = NULL; } GNUNET_free (ekr->exchange_url); GNUNET_free (ekr->payto_uri); GNUNET_free (ekr); } if (NULL != kc->timeout_task) { GNUNET_SCHEDULER_cancel (kc->timeout_task); kc->timeout_task = NULL; } if (NULL != kc->response) { MHD_destroy_response (kc->response); kc->response = NULL; } GNUNET_CONTAINER_DLL_remove (kc_head, kc_tail, kc); json_decref (kc->pending_kycs); json_decref (kc->timeout_kycs); GNUNET_free (kc); } /** * Resume the given KYC context and send the given response. * Stores the response in the @a kc and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param kc KYC context * @param response_code response code to use * @param response response data to send back */ static void resume_kyc_with_response (struct KycContext *kc, unsigned int response_code, struct MHD_Response *response) { char dat[128]; kc->response_code = response_code; kc->response = response; switch (response_code) { case MHD_HTTP_OK: /* KYC failed, cache briefly */ TALER_MHD_get_date_string (GNUNET_TIME_relative_to_absolute ( EXPIRATION_KYC_FAILURE), dat); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_EXPIRES, dat)); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_CACHE_CONTROL, "max-age=300")); break; case MHD_HTTP_NO_CONTENT: /* KYC passed, cache for a long time! */ TALER_MHD_get_date_string (GNUNET_TIME_relative_to_absolute ( EXPIRATION_KYC_SUCCESS), dat); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_EXPIRES, dat)); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_CACHE_CONTROL, "max-age=3600")); break; case MHD_HTTP_BAD_GATEWAY: case MHD_HTTP_GATEWAY_TIMEOUT: break; /* no caching */ } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /kyc handling as exchange interaction is done (%u)\n", response_code); if (NULL != kc->timeout_task) { GNUNET_SCHEDULER_cancel (kc->timeout_task); kc->timeout_task = NULL; } GNUNET_assert (GNUNET_YES == kc->suspended); kc->suspended = GNUNET_NO; MHD_resume_connection (kc->connection); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } /** * Handle a timeout for the processing of the kyc request. * * @param cls our `struct KycContext` */ static void handle_kyc_timeout (void *cls) { struct KycContext *kc = cls; struct ExchangeKycRequest *ekr; kc->timeout_task = NULL; while (NULL != (ekr = kc->exchange_pending_head)) { GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, kc->exchange_pending_tail, ekr); if (NULL != ekr->kyc) { TALER_EXCHANGE_kyc_check_cancel (ekr->kyc); ekr->kyc = NULL; } if (NULL != ekr->fo) { TMH_EXCHANGES_find_exchange_cancel (ekr->fo); ekr->fo = NULL; } GNUNET_assert ( 0 == json_array_append_new ( kc->timeout_kycs, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("exchange_url", ekr->exchange_url), GNUNET_JSON_pack_uint64 ("exchange_code", TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT), GNUNET_JSON_pack_uint64 ("exchange_http_status", 0)))); GNUNET_free (ekr->exchange_url); GNUNET_free (ekr->payto_uri); GNUNET_free (ekr); } GNUNET_assert (GNUNET_YES == kc->suspended); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming KYC with gateway timeout\n"); resume_kyc_with_response ( kc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_array_incref ("pending_kycs", kc->pending_kycs), GNUNET_JSON_pack_array_incref ("timeout_kycs", kc->timeout_kycs))); } /** * We are done with the KYC request @a ekr. * Remove it from the work list and check if * we are done overall. * * @param[in] ekr key request that is done (and will be freed) */ static void ekr_finished (struct ExchangeKycRequest *ekr) { struct KycContext *kc = ekr->kc; GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, kc->exchange_pending_tail, ekr); GNUNET_free (ekr->exchange_url); GNUNET_free (ekr->payto_uri); GNUNET_free (ekr); if (NULL != kc->exchange_pending_head) return; /* wait for more */ /* All exchange requests done, create final big response from cummulated replies */ if ( (0 == json_array_size (kc->pending_kycs)) && (0 == json_array_size (kc->timeout_kycs)) ) { /* special case: all KYC operations did succeed after we asked at the exchanges => 204 */ struct MHD_Response *response; response = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); resume_kyc_with_response (kc, MHD_HTTP_NO_CONTENT, response); return; } resume_kyc_with_response ( kc, kc->response_code, /* MHD_HTTP_OK or MHD_HTTP_BAD_GATEWAY */ TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_array_incref ("pending_kycs", kc->pending_kycs), GNUNET_JSON_pack_array_incref ("timeout_kycs", kc->timeout_kycs))); } /** * Function called with the result of a KYC check. * * @param cls a `struct ExchangeKycRequest *` * @param ks the account's KYC status details */ static void exchange_check_cb (void *cls, const struct TALER_EXCHANGE_KycStatus *ks) { struct ExchangeKycRequest *ekr = cls; struct KycContext *kc = ekr->kc; ekr->kyc = NULL; switch (ks->http_status) { case MHD_HTTP_OK: { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->account_kyc_set_status (TMH_db->cls, kc->mi->settings.id, &ekr->h_wire, ekr->exchange_url, ekr->exchange_kyc_serial, &ks->details.success.exchange_sig, &ks->details.success.exchange_pub, ks->details.success.timestamp, true); if (qs < 0) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to store KYC status in database!\n"); } } break; case MHD_HTTP_ACCEPTED: GNUNET_assert ( 0 == json_array_append_new ( kc->pending_kycs, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("kyc_url", ks->details.accepted.kyc_url), GNUNET_JSON_pack_string ("exchange_url", ekr->exchange_url), GNUNET_JSON_pack_string ("payto_uri", ekr->payto_uri)))); break; case MHD_HTTP_NO_CONTENT: { struct GNUNET_TIME_Timestamp now; enum GNUNET_DB_QueryStatus qs; now = GNUNET_TIME_timestamp_get (); qs = TMH_db->account_kyc_set_status (TMH_db->cls, kc->mi->settings.id, &ekr->h_wire, ekr->exchange_url, ekr->exchange_kyc_serial, NULL, NULL, now, true); if (qs < 0) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to store KYC status in database!\n"); } } break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Exchange responded with HTTP status %u (%d) to /kyc-check request!\n", ks->http_status, ks->ec); kc->response_code = MHD_HTTP_BAD_GATEWAY; GNUNET_assert ( 0 == json_array_append_new ( kc->timeout_kycs, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("exchange_url", ekr->exchange_url), GNUNET_JSON_pack_uint64 ("exchange_code", ks->ec), GNUNET_JSON_pack_uint64 ("exchange_http_status", ks->http_status)))); } ekr_finished (ekr); } /** * Function called with the result of a #TMH_EXCHANGES_find_exchange() * operation. Runs the KYC check against the exchange. * * @param cls closure with our `struct ExchangeKycRequest *` * @param hr HTTP response details * @param eh handle to the exchange context * @param payto_uri payto://-URI of the exchange * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted true if this exchange is trusted by config */ static void kyc_with_exchange (void *cls, const struct TALER_EXCHANGE_HttpResponse *hr, struct TALER_EXCHANGE_Handle *eh, const char *payto_uri, const struct TALER_Amount *wire_fee, bool exchange_trusted) { struct ExchangeKycRequest *ekr = cls; struct KycContext *kc = ekr->kc; struct TALER_PaytoHashP h_payto; (void) payto_uri; (void) wire_fee; (void) exchange_trusted; ekr->fo = NULL; if (MHD_HTTP_OK != hr->http_status) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Exchange responded with HTTP status %u (%d) to /kyc-check request!\n", hr->http_status, hr->ec); kc->response_code = MHD_HTTP_BAD_GATEWAY; GNUNET_assert ( 0 == json_array_append_new ( kc->timeout_kycs, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("exchange_url", ekr->exchange_url), GNUNET_JSON_pack_uint64 ("exchange_code", hr->ec), GNUNET_JSON_pack_uint64 ("exchange_http_status", hr->http_status)))); ekr_finished (ekr); return; } TALER_payto_hash (ekr->payto_uri, &h_payto); ekr->kyc = TALER_EXCHANGE_kyc_check ( eh, ekr->exchange_kyc_serial, &h_payto, /* FIXME: get from settings! */ TALER_KYCLOGIC_KYC_UT_BUSINESS, kc->timeout, &exchange_check_cb, ekr); } /** * Function called from ``account_kyc_get_status`` * with KYC status information for this merchant. * * @param cls our `struct KycContext *` * @param h_wire hash of the wire account * @param exchange_kyc_serial serial number for the KYC process at the exchange, 0 if unknown * @param payto_uri payto:// URI of the merchant's bank account * @param exchange_url base URL of the exchange for which this is a status * @param last_check when did we last get an update on our KYC status from the exchange * @param kyc_ok true if we satisfied the KYC requirements */ static void kyc_status_cb (void *cls, const struct TALER_MerchantWireHashP *h_wire, uint64_t exchange_kyc_serial, const char *payto_uri, const char *exchange_url, struct GNUNET_TIME_Timestamp last_check, bool kyc_ok) { struct KycContext *kc = cls; struct ExchangeKycRequest *ekr; if (kyc_ok && (GNUNET_TIME_relative_cmp ( GNUNET_TIME_absolute_get_duration (last_check.abs_time), <, STALE_KYC_TIMEOUT)) ) return; /* KYC ok, ignore! */ if (0 == exchange_kyc_serial) { kc->kyc_serial_pending = true; return; } kc->response_code = MHD_HTTP_ACCEPTED; ekr = GNUNET_new (struct ExchangeKycRequest); GNUNET_CONTAINER_DLL_insert (kc->exchange_pending_head, kc->exchange_pending_tail, ekr); ekr->h_wire = *h_wire; ekr->exchange_kyc_serial = exchange_kyc_serial; ekr->exchange_url = GNUNET_strdup (exchange_url); ekr->payto_uri = GNUNET_strdup (payto_uri); ekr->last_check = last_check; ekr->kyc_ok = kyc_ok; ekr->kc = kc; ekr->fo = TMH_EXCHANGES_find_exchange (exchange_url, NULL, GNUNET_NO, &kyc_with_exchange, ekr); } /** * Check the KYC status of an instance. * * @param mi instance to check KYC status of * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request * @return MHD result code */ static MHD_RESULT get_instances_ID_kyc (struct TMH_MerchantInstance *mi, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct KycContext *kc = hc->ctx; if (NULL == kc) { kc = GNUNET_new (struct KycContext); kc->mi = mi; hc->ctx = kc; hc->cc = &kyc_context_cleanup; GNUNET_CONTAINER_DLL_insert (kc_head, kc_tail, kc); kc->connection = connection; kc->hc = hc; kc->pending_kycs = json_array (); GNUNET_assert (NULL != kc->pending_kycs); kc->timeout_kycs = json_array (); GNUNET_assert (NULL != kc->timeout_kycs); /* process 'timeout_ms' argument */ { const char *long_poll_timeout_s; long_poll_timeout_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "timeout_ms"); if (NULL != long_poll_timeout_s) { unsigned int timeout_ms; char dummy; if (1 != sscanf (long_poll_timeout_s, "%u%c", &timeout_ms, &dummy)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "timeout_ms must be non-negative number"); } kc->timeout = GNUNET_TIME_relative_multiply ( GNUNET_TIME_UNIT_MILLISECONDS, timeout_ms); kc->timeout_task = GNUNET_SCHEDULER_add_delayed (kc->timeout, &handle_kyc_timeout, kc); } } /* end timeout processing */ /* process 'exchange_url' argument */ kc->exchange_url = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "exchange_url"); if ( (NULL != kc->exchange_url) && (! TALER_url_valid_charset (kc->exchange_url) || ( (0 != strncasecmp (kc->exchange_url, "http://", strlen ("http://"))) && (0 != strncasecmp (kc->exchange_url, "https://", strlen ("https://"))) ) ) ) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "exchange_url must be a valid HTTP(s) URL"); } /* process 'h_wire' argument */ { const char *h_wire_s; h_wire_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "h_wire"); if (NULL != h_wire_s) { if (GNUNET_OK != GNUNET_STRINGS_string_to_data (h_wire_s, strlen (h_wire_s), &kc->h_wire, sizeof (kc->h_wire))) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "h_wire must be Crockford base32 encoded hash"); } kc->have_h_wire = true; } } /* end of h_wire processing */ /* Check our database */ { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->account_kyc_get_status (TMH_db->cls, mi->settings.id, kc->have_h_wire ? &kc->h_wire : NULL, kc->exchange_url, &kyc_status_cb, kc); if (qs < 0) { GNUNET_break (0); return TALER_MHD_reply_with_ec (connection, TALER_EC_GENERIC_DB_FETCH_FAILED, "account_kyc_get_status"); } } if (kc->kyc_serial_pending) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange legitimization UUID unknown, assuming KYC pending\n"); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_SERVICE_UNAVAILABLE, GNUNET_JSON_pack_string ("hint", "awaiting legitimization UUID")); } if (NULL == kc->exchange_pending_head) return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); MHD_suspend_connection (connection); kc->suspended = GNUNET_YES; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending KYC request handling while checking with the exchange(s)\n"); return MHD_YES; } if (GNUNET_SYSERR == kc->suspended) return MHD_NO; /* during shutdown, we don't generate any more replies */ GNUNET_assert (GNUNET_NO == kc->suspended); if (0 != kc->response_code) { /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == kc->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } return MHD_queue_response (connection, kc->response_code, kc->response); } /* we should never get here */ GNUNET_break (0); return MHD_NO; } MHD_RESULT TMH_private_get_instances_ID_kyc (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; (void) rh; return get_instances_ID_kyc (mi, connection, hc); } MHD_RESULT TMH_private_get_instances_default_ID_kyc (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi; (void) rh; mi = TMH_lookup_instance (hc->infix); if (NULL == mi) { return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, hc->infix); } return get_instances_ID_kyc (mi, connection, hc); } /* end of taler-merchant-httpd_private-get-instances-ID-kyc.c */