taler-exchange-httpd_kyc-webhook.c (12108B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-2024 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_kyc-webhook.c 18 * @brief Handle notification of KYC completion via webhook. 19 * @author Christian Grothoff 20 */ 21 #include <gnunet/gnunet_util_lib.h> 22 #include <gnunet/gnunet_json_lib.h> 23 #include <jansson.h> 24 #include <microhttpd.h> 25 #include <pthread.h> 26 #include "taler/taler_json_lib.h" 27 #include "taler/taler_mhd_lib.h" 28 #include "taler/taler_kyclogic_lib.h" 29 #include "taler-exchange-httpd_common_kyc.h" 30 #include "taler-exchange-httpd_kyc-webhook.h" 31 #include "taler-exchange-httpd_responses.h" 32 #include "exchange-database/get_kyc_provider_account.h" 33 34 35 /** 36 * Context for the webhook. 37 */ 38 struct KycWebhookContext 39 { 40 41 /** 42 * Kept in a DLL while suspended. 43 */ 44 struct KycWebhookContext *next; 45 46 /** 47 * Kept in a DLL while suspended. 48 */ 49 struct KycWebhookContext *prev; 50 51 /** 52 * Details about the connection we are processing. 53 */ 54 struct TEH_RequestContext *rc; 55 56 /** 57 * Handle for the KYC-AML trigger interaction. 58 */ 59 struct TEH_KycMeasureRunContext *kat; 60 61 /** 62 * Plugin responsible for the webhook. 63 */ 64 struct TALER_KYCLOGIC_Plugin *plugin; 65 66 /** 67 * Name of the KYC provider (suffix of the 68 * section name in the configuration). 69 */ 70 const char *provider_name; 71 72 /** 73 * Configuration for the specific action. 74 */ 75 struct TALER_KYCLOGIC_ProviderDetails *pd; 76 77 /** 78 * Webhook activity. 79 */ 80 struct TALER_KYCLOGIC_WebhookHandle *wh; 81 82 /** 83 * Final HTTP response to return. 84 */ 85 struct MHD_Response *response; 86 87 /** 88 * Final HTTP response code to return. 89 */ 90 unsigned int response_code; 91 92 /** 93 * Response from the webhook plugin. 94 * 95 * Will become the final response on successfully 96 * running the measure with the new attributes. 97 */ 98 struct MHD_Response *webhook_response; 99 100 /** 101 * Response code to return for the webhook plugin 102 * response. 103 */ 104 unsigned int webhook_response_code; 105 106 /** 107 * #GNUNET_YES if we are suspended, 108 * #GNUNET_NO if not. 109 * #GNUNET_SYSERR if we had some error. 110 */ 111 enum GNUNET_GenericReturnValue suspended; 112 113 }; 114 115 116 /** 117 * Contexts are kept in a DLL while suspended. 118 */ 119 static struct KycWebhookContext *kwh_head; 120 121 /** 122 * Contexts are kept in a DLL while suspended. 123 */ 124 static struct KycWebhookContext *kwh_tail; 125 126 127 /** 128 * Resume processing the @a kwh request. 129 * 130 * @param kwh request to resume 131 */ 132 static void 133 kwh_resume (struct KycWebhookContext *kwh) 134 { 135 GNUNET_assert (GNUNET_YES == kwh->suspended); 136 kwh->suspended = GNUNET_NO; 137 GNUNET_CONTAINER_DLL_remove (kwh_head, 138 kwh_tail, 139 kwh); 140 MHD_resume_connection (kwh->rc->connection); 141 TALER_MHD_daemon_trigger (); 142 } 143 144 145 void 146 TEH_kyc_webhook_cleanup (void) 147 { 148 struct KycWebhookContext *kwh; 149 150 while (NULL != (kwh = kwh_head)) 151 { 152 if (NULL != kwh->wh) 153 { 154 kwh->plugin->webhook_cancel (kwh->wh); 155 kwh->wh = NULL; 156 } 157 kwh_resume (kwh); 158 } 159 } 160 161 162 /** 163 * Function called after the KYC-AML trigger is done. 164 * 165 * @param cls closure with a `struct KycWebhookContext *` 166 * @param ec error code or 0 on success 167 * @param detail error message or NULL on success / no info 168 */ 169 static void 170 kyc_aml_webhook_finished ( 171 void *cls, 172 enum TALER_ErrorCode ec, 173 const char *detail) 174 { 175 struct KycWebhookContext *kwh = cls; 176 177 kwh->kat = NULL; 178 GNUNET_assert (NULL == kwh->response); 179 if (TALER_EC_NONE != ec) 180 { 181 kwh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; 182 kwh->response = TALER_MHD_make_error ( 183 ec, 184 detail 185 ); 186 } 187 else 188 { 189 GNUNET_assert (NULL != kwh->webhook_response); 190 kwh->response_code = kwh->webhook_response_code; 191 kwh->response = kwh->webhook_response; 192 kwh->webhook_response = NULL; 193 kwh->webhook_response_code = 0; 194 } 195 kwh_resume (kwh); 196 } 197 198 199 /** 200 * Function called with the result of a KYC webhook operation. 201 * 202 * Note that the "decref" for the @a response 203 * will be done by the plugin. 204 * 205 * @param cls closure 206 * @param process_row legitimization process the webhook was about 207 * @param account_id account the webhook was about 208 * @param is_wallet true if @a account_id is for a wallet 209 * @param provider_name name of the KYC provider that was run 210 * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown 211 * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown 212 * @param status KYC status 213 * @param expiration until when is the KYC check valid 214 * @param attributes user attributes returned by the provider 215 * @param http_status HTTP status code of @a response 216 * @param[in] response to return to the HTTP client 217 */ 218 static void 219 webhook_finished_cb ( 220 void *cls, 221 uint64_t process_row, 222 const struct TALER_NormalizedPaytoHashP *account_id, 223 bool is_wallet, 224 const char *provider_name, 225 const char *provider_user_id, 226 const char *provider_legitimization_id, 227 enum TALER_KYCLOGIC_KycStatus status, 228 struct GNUNET_TIME_Absolute expiration, 229 const json_t *attributes, 230 unsigned int http_status, 231 struct MHD_Response *response) 232 { 233 struct KycWebhookContext *kwh = cls; 234 enum GNUNET_DB_QueryStatus qs; 235 236 kwh->wh = NULL; 237 kwh->webhook_response = response; 238 kwh->webhook_response_code = http_status; 239 240 switch (status) 241 { 242 case TALER_KYCLOGIC_STATUS_SUCCESS: 243 GNUNET_assert (json_is_string (json_object_get (attributes, 244 "FORM_ID"))); 245 qs = TEH_kyc_store_attributes ( 246 process_row, 247 account_id, 248 provider_name, 249 provider_user_id, 250 provider_legitimization_id, 251 expiration, 252 attributes); 253 if (0 >= qs) 254 { 255 GNUNET_break (0); 256 kyc_aml_webhook_finished (kwh, 257 TALER_EC_GENERIC_DB_STORE_FAILED, 258 "kyc_store_attributes"); 259 return; 260 } 261 kwh->kat = TEH_kyc_run_measure_for_attributes ( 262 &kwh->rc->async_scope_id, 263 process_row, 264 account_id, 265 is_wallet, 266 &kyc_aml_webhook_finished, 267 kwh); 268 if (NULL == kwh->kat) 269 { 270 kyc_aml_webhook_finished (kwh, 271 TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION, 272 "[exchange] AML_KYC_TRIGGER"); 273 } 274 break; 275 case TALER_KYCLOGIC_STATUS_FAILED: 276 case TALER_KYCLOGIC_STATUS_PROVIDER_FAILED: 277 case TALER_KYCLOGIC_STATUS_USER_ABORTED: 278 case TALER_KYCLOGIC_STATUS_ABORTED: 279 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 280 "KYC process %s/%s (Row #%llu) failed: %d\n", 281 provider_user_id, 282 provider_legitimization_id, 283 (unsigned long long) process_row, 284 status); 285 if (! TEH_kyc_failed ( 286 process_row, 287 account_id, 288 provider_name, 289 provider_user_id, 290 provider_legitimization_id, 291 TALER_KYCLOGIC_status2s (status), 292 TALER_EC_EXCHANGE_GENERIC_KYC_FAILED)) 293 { 294 GNUNET_break (0); 295 kyc_aml_webhook_finished (kwh, 296 TALER_EC_GENERIC_DB_STORE_FAILED, 297 "TEH_kyc_failed"); 298 break; 299 } 300 kyc_aml_webhook_finished (kwh, 301 TALER_EC_NONE, 302 NULL); 303 break; 304 default: 305 GNUNET_log ( 306 GNUNET_ERROR_TYPE_INFO, 307 "KYC status of %s/%s (Row #%llu) is %d\n", 308 provider_user_id, 309 provider_legitimization_id, 310 (unsigned long long) process_row, 311 (int) status); 312 kyc_aml_webhook_finished (kwh, 313 TALER_EC_NONE, 314 NULL); 315 break; 316 } 317 } 318 319 320 /** 321 * Function called to clean up a context. 322 * 323 * @param rc request context 324 */ 325 static void 326 clean_kwh (struct TEH_RequestContext *rc) 327 { 328 struct KycWebhookContext *kwh = rc->rh_ctx; 329 330 if (NULL != kwh->wh) 331 { 332 kwh->plugin->webhook_cancel (kwh->wh); 333 kwh->wh = NULL; 334 } 335 if (NULL != kwh->kat) 336 { 337 TEH_kyc_run_measure_cancel (kwh->kat); 338 kwh->kat = NULL; 339 } 340 if (NULL != kwh->response) 341 { 342 MHD_destroy_response (kwh->response); 343 kwh->response = NULL; 344 } 345 if (NULL != kwh->webhook_response) 346 { 347 MHD_destroy_response (kwh->webhook_response); 348 kwh->webhook_response = NULL; 349 } 350 GNUNET_free (kwh); 351 } 352 353 354 /** 355 * Handle a (GET or POST) "/kyc-webhook" request. 356 * 357 * @param rc request to handle 358 * @param method HTTP request method used by the client 359 * @param root uploaded JSON body (can be NULL) 360 * @param args one argument with the legitimization_uuid 361 * @return MHD result code 362 */ 363 static enum MHD_Result 364 handler_kyc_webhook_generic ( 365 struct TEH_RequestContext *rc, 366 const char *method, 367 const json_t *root, 368 const char *const args[]) 369 { 370 struct KycWebhookContext *kwh = rc->rh_ctx; 371 372 if (NULL == kwh) 373 { /* first time */ 374 kwh = GNUNET_new (struct KycWebhookContext); 375 kwh->rc = rc; 376 rc->rh_ctx = kwh; 377 rc->rh_cleaner = &clean_kwh; 378 379 if ( (NULL == args[0]) || 380 (GNUNET_OK != 381 TALER_KYCLOGIC_lookup_logic ( 382 args[0], 383 &kwh->plugin, 384 &kwh->pd, 385 &kwh->provider_name)) ) 386 { 387 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 388 "KYC logic `%s' unknown (check KYC provider configuration)\n", 389 args[0]); 390 return TALER_MHD_reply_with_error ( 391 rc->connection, 392 MHD_HTTP_NOT_FOUND, 393 TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, 394 args[0]); 395 } 396 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 397 "KYC logic `%s' mapped to section %s\n", 398 args[0], 399 kwh->provider_name); 400 kwh->wh = kwh->plugin->webhook ( 401 kwh->plugin->cls, 402 kwh->pd, 403 TALER_EXCHANGEDB_get_kyc_provider_account, 404 TEH_pg, 405 method, 406 &args[1], 407 rc->connection, 408 root, 409 &webhook_finished_cb, 410 kwh); 411 if (NULL == kwh->wh) 412 { 413 GNUNET_break_op (0); 414 return TALER_MHD_reply_with_error ( 415 rc->connection, 416 MHD_HTTP_INTERNAL_SERVER_ERROR, 417 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 418 "failed to run webhook logic"); 419 } 420 kwh->suspended = GNUNET_YES; 421 GNUNET_CONTAINER_DLL_insert (kwh_head, 422 kwh_tail, 423 kwh); 424 MHD_suspend_connection (rc->connection); 425 return MHD_YES; 426 } 427 GNUNET_break (GNUNET_NO == kwh->suspended); 428 429 if (NULL != kwh->response) 430 { 431 enum MHD_Result res; 432 433 res = MHD_queue_response (rc->connection, 434 kwh->response_code, 435 kwh->response); 436 GNUNET_break (MHD_YES == res); 437 return res; 438 } 439 440 /* We resumed, but got no response? This should 441 not happen. */ 442 GNUNET_break (0); 443 return TALER_MHD_reply_with_error ( 444 rc->connection, 445 MHD_HTTP_INTERNAL_SERVER_ERROR, 446 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 447 "resumed without response"); 448 } 449 450 451 enum MHD_Result 452 TEH_handler_kyc_webhook_get ( 453 struct TEH_RequestContext *rc, 454 const char *const args[]) 455 { 456 return handler_kyc_webhook_generic ( 457 rc, 458 MHD_HTTP_METHOD_GET, 459 NULL, 460 args); 461 } 462 463 464 enum MHD_Result 465 TEH_handler_kyc_webhook_post ( 466 struct TEH_RequestContext *rc, 467 const json_t *root, 468 const char *const args[]) 469 { 470 return handler_kyc_webhook_generic ( 471 rc, 472 MHD_HTTP_METHOD_POST, 473 root, 474 args); 475 } 476 477 478 /* end of taler-exchange-httpd_kyc-webhook.c */