/* This file is part of TALER Copyright (C) 2022, 2024 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-kyc-tester.c * @brief tool to test KYC integrations * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_templating_lib.h" #include "taler_util.h" #include "taler_kyclogic_lib.h" #include "taler_kyclogic_plugin.h" #include /** * @brief Context in which the exchange is processing * all requests */ struct TEKT_RequestContext { /** * Opaque parsing context. */ void *opaque_post_parsing_context; /** * Request handler responsible for this request. */ const struct TEKT_RequestHandler *rh; /** * Request URL (for logging). */ const char *url; /** * Connection we are processing. */ struct MHD_Connection *connection; /** * HTTP response to return (or NULL). */ struct MHD_Response *response; /** * @e rh-specific cleanup routine. Function called * upon completion of the request that should * clean up @a rh_ctx. Can be NULL. */ void (*rh_cleaner)(struct TEKT_RequestContext *rc); /** * @e rh-specific context. Place where the request * handler can associate state with this request. * Can be NULL. */ void *rh_ctx; /** * Uploaded JSON body, if any. */ json_t *root; /** * HTTP status to return upon resume if @e response * is non-NULL. */ unsigned int http_status; }; /** * @brief Struct describing an URL and the handler for it. */ struct TEKT_RequestHandler { /** * URL the handler is for (first part only). */ const char *url; /** * Method the handler is for. */ const char *method; /** * Callbacks for handling of the request. Which one is used * depends on @e method. */ union { /** * Function to call to handle a GET requests (and those * with @e method NULL). * * @param rc context for the request * @param mime_type the @e mime_type for the reply (hint, can be NULL) * @param args array of arguments, needs to be of length @e args_expected * @return MHD result code */ MHD_RESULT (*get)(struct TEKT_RequestContext *rc, const char *const args[]); /** * Function to call to handle a POST request. * * @param rc context for the request * @param json uploaded JSON data * @param args array of arguments, needs to be of length @e args_expected * @return MHD result code */ MHD_RESULT (*post)(struct TEKT_RequestContext *rc, const json_t *root, const char *const args[]); } handler; /** * Number of arguments this handler expects in the @a args array. */ unsigned int nargs; /** * Is the number of arguments given in @e nargs only an upper bound, * and calling with fewer arguments could be OK? */ bool nargs_is_upper_bound; /** * Mime type to use in reply (hint, can be NULL). */ const char *mime_type; /** * Raw data for the @e handler, can be NULL for none provided. */ const void *data; /** * Number of bytes in @e data, 0 for data is 0-terminated (!). */ size_t data_size; /** * Default response code. 0 for none provided. */ unsigned int response_code; }; /** * Information we track per ongoing kyc-proof request. */ struct ProofRequestState { /** * Kept in a DLL. */ struct ProofRequestState *next; /** * Kept in a DLL. */ struct ProofRequestState *prev; /** * Handle for operation with the plugin. */ struct TALER_KYCLOGIC_ProofHandle *ph; /** * Logic plugin we are using. */ struct TALER_KYCLOGIC_Plugin *logic; /** * HTTP request details. */ struct TEKT_RequestContext *rc; }; /** * Head of DLL. */ static struct ProofRequestState *rs_head; /** * Tail of DLL. */ static struct ProofRequestState *rs_tail; /** * The exchange's configuration (global) */ static const struct GNUNET_CONFIGURATION_Handle *TEKT_cfg; /** * Handle to the HTTP server. */ static struct MHD_Daemon *mhd; /** * Our base URL. */ static char *TEKT_base_url; /** * Payto set via command-line (or otherwise random). */ static struct TALER_PaytoHashP cmd_line_h_payto; /** * Provider user ID to use. */ static char *cmd_provider_user_id; /** * Provider legitimization ID to use. */ static char *cmd_provider_legitimization_id; /** * Custom legitimization rule in JSON given as * a string. */ static char *lrs_s; /** * Type of the operation that triggers legitimization. */ static char *operation_s; /** * Amount threshold crossed that triggers some rule. */ static struct TALER_Amount trigger_amount; /** * Row ID to use, override with '-r' */ static unsigned int kyc_row_id = 42; /** * -P command-line option. */ static int print_h_payto; /** * -w command-line option. */ static int run_webservice; /** * -M command-line option. */ static int list_measures; /** * Value to return from main() */ static int global_ret; /** * -m command-line flag. */ static char *measure; /** * Handle for ongoing initiation operation. */ static struct TALER_KYCLOGIC_InitiateHandle *ih; /** * KYC logic running for @e ih. */ static struct TALER_KYCLOGIC_Plugin *ih_logic; /** * Port to run the daemon on. */ static uint16_t serve_port; /** * Context for all CURL operations (useful to the event loop) */ static struct GNUNET_CURL_Context *TEKT_curl_ctx; /** * Context for integrating #TEKT_curl_ctx with the * GNUnet event loop. */ static struct GNUNET_CURL_RescheduleContext *exchange_curl_rc; /** * 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 TEKT_RequestContext *rc; /** * Plugin responsible for the webhook. */ struct TALER_KYCLOGIC_Plugin *plugin; /** * 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; /** * Name of the configuration * section defining the KYC logic. */ const char *section_name; /** * 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); } static void 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 with the result of a webhook * operation. * * Note that the "decref" for the @a response * will be done by the plugin. * * @param cls closure * @param process_row legitimization process request the webhook was about * @param account_id account the webhook was about * @param provider_section configuration section of the logic * @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; (void) expiration; (void) provider_section; kwh->wh = NULL; if ( (NULL != account_id) && (0 != GNUNET_memcmp (account_id, &cmd_line_h_payto)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received webhook for unexpected account\n"); } if ( (NULL != provider_user_id) && (NULL != cmd_provider_user_id) && (0 != strcmp (provider_user_id, cmd_provider_user_id)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received webhook for unexpected provider user ID (%s)\n", provider_user_id); } if ( (NULL != provider_legitimization_id) && (NULL != cmd_provider_legitimization_id) && (0 != strcmp (provider_legitimization_id, cmd_provider_legitimization_id)) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Received webhook for unexpected provider legitimization ID (%s)\n", provider_legitimization_id); } switch (status) { case TALER_KYCLOGIC_STATUS_SUCCESS: /* _successfully_ resumed case */ GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "KYC successful for user `%s' (legi: %s)\n", provider_user_id, provider_legitimization_id); GNUNET_break (NULL != attributes); fprintf (stderr, "Extracted attributes:\n"); json_dumpf (attributes, stderr, JSON_INDENT (2)); break; default: GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC status of %s/%s (process #%llu) is %d\n", provider_user_id, provider_legitimization_id, (unsigned long long) process_row, status); break; } kwh->response = response; kwh->response_code = http_status; kwh_resume (kwh); TALER_MHD_daemon_trigger (); } /** * Function called to clean up a context. * * @param rc request context */ static void clean_kwh (struct TEKT_RequestContext *rc) { struct KycWebhookContext *kwh = rc->rh_ctx; if (NULL != kwh->wh) { kwh->plugin->webhook_cancel (kwh->wh); kwh->wh = NULL; } if (NULL != kwh->response) { MHD_destroy_response (kwh->response); kwh->response = NULL; } GNUNET_free (kwh); } /** * Function the plugin can use to lookup an * @a h_payto by @a provider_legitimization_id. * * @param cls closure, NULL * @param provider_section * @param provider_legitimization_id legi to look up * @param[out] h_payto where to write the result * @param[out] legi_row where to write the row ID for the legitimization ID * @return database transaction status */ static enum GNUNET_DB_QueryStatus kyc_provider_account_lookup ( void *cls, const char *provider_section, const char *provider_legitimization_id, struct TALER_PaytoHashP *h_payto, uint64_t *legi_row) { (void) cls; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Simulated account lookup using `%s/%s'\n", provider_section, provider_legitimization_id); *h_payto = cmd_line_h_payto; *legi_row = kyc_row_id; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } /** * 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 TEKT_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->section_name)) ) { 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, "Calling KYC provider specific webhook\n"); kwh->wh = kwh->plugin->webhook (kwh->plugin->cls, kwh->pd, &kyc_provider_account_lookup, NULL, 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; } if (NULL != kwh->response) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning queued reply for KWH\n"); /* handle _failed_ resumed cases */ return MHD_queue_response (rc->connection, kwh->response_code, kwh->response); } /* We resumed, but got no response? This should not happen. */ GNUNET_assert (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, "resumed without response"); } /** * Handle a GET "/kyc-webhook" request. * * @param rc request to handle * @param args one argument with the legitimization_uuid * @return MHD result code */ static MHD_RESULT handler_kyc_webhook_get ( struct TEKT_RequestContext *rc, const char *const args[]) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Webhook GET triggered\n"); return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_GET, NULL, args); } /** * Handle a POST "/kyc-webhook" request. * * @param rc request to handle * @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_post ( struct TEKT_RequestContext *rc, const json_t *root, const char *const args[]) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Webhook POST triggered\n"); return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_POST, root, args); } /** * 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 with the `struct ProofRequestState` * @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 attributes about the user * @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 ProofRequestState *rs = cls; (void) expiration; GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "KYC legitimization %s completed with status %d (%u) for %s\n", provider_legitimization_id, status, http_status, provider_user_id); if (TALER_KYCLOGIC_STATUS_SUCCESS == status) { GNUNET_break (NULL != attributes); fprintf (stderr, "Extracted attributes:\n"); json_dumpf (attributes, stderr, JSON_INDENT (2)); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Returning response %p with status %u\n", response, http_status); rs->rc->response = response; rs->rc->http_status = http_status; GNUNET_CONTAINER_DLL_remove (rs_head, rs_tail, rs); MHD_resume_connection (rs->rc->connection); TALER_MHD_daemon_trigger (); GNUNET_free (rs); } /** * Function called when we receive a 'GET' to the * '/kyc-proof' endpoint. * * @param rc request context * @param args remaining URL arguments; * args[0] should be the logic plugin name */ static MHD_RESULT handler_kyc_proof_get ( struct TEKT_RequestContext *rc, const char *const args[1]) { struct TALER_PaytoHashP h_payto; struct TALER_KYCLOGIC_ProviderDetails *pd; struct TALER_KYCLOGIC_Plugin *logic; struct ProofRequestState *rs; const char *section_name; const char *h_paytos; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "GET /kyc-proof triggered\n"); if (NULL == args[0]) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, "'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required"); } h_paytos = MHD_lookup_connection_value (rc->connection, MHD_GET_ARGUMENT_KIND, "state"); if (NULL == h_paytos) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MISSING, "h_payto"); } if (GNUNET_OK != GNUNET_STRINGS_string_to_data (h_paytos, strlen (h_paytos), &h_payto, sizeof (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 (0 != GNUNET_memcmp (&h_payto, &cmd_line_h_payto)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, "h_payto"); } if (GNUNET_OK != TALER_KYCLOGIC_lookup_logic (args[0], &logic, &pd, §ion_name)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not initiate KYC with provider `%s' (configuration error?)\n", args[0]); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, args[0]); } rs = GNUNET_new (struct ProofRequestState); rs->rc = rc; rs->logic = logic; MHD_suspend_connection (rc->connection); GNUNET_CONTAINER_DLL_insert (rs_head, rs_tail, rs); rs->ph = logic->proof (logic->cls, pd, rc->connection, &h_payto, kyc_row_id, cmd_provider_user_id, cmd_provider_legitimization_id, &proof_cb, rs); GNUNET_assert (NULL != rs->ph); return MHD_YES; } /** * Function called whenever MHD is done with a request. If the * request was a POST, we may have stored a `struct Buffer *` in the * @a con_cls that might still need to be cleaned up. Call the * respective function to free the memory. * * @param cls client-defined closure * @param connection connection handle * @param con_cls value as set by the last call to * the #MHD_AccessHandlerCallback * @param toe reason for request termination * @see #MHD_OPTION_NOTIFY_COMPLETED * @ingroup request */ static void handle_mhd_completion_callback (void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { struct TEKT_RequestContext *rc = *con_cls; (void) cls; if (NULL == rc) return; if (NULL != rc->rh_cleaner) rc->rh_cleaner (rc); { #if MHD_VERSION >= 0x00097304 const union MHD_ConnectionInfo *ci; unsigned int http_status = 0; ci = MHD_get_connection_info (connection, MHD_CONNECTION_INFO_HTTP_STATUS); if (NULL != ci) http_status = ci->http_status; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Request for `%s' completed with HTTP status %u (%d)\n", rc->url, http_status, toe); #else (void) connection; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Request for `%s' completed (%d)\n", rc->url, toe); #endif } TALER_MHD_parse_post_cleanup_callback (rc->opaque_post_parsing_context); /* Sanity-check that we didn't leave any transactions hanging */ if (NULL != rc->root) json_decref (rc->root); GNUNET_free (rc); *con_cls = NULL; } /** * We found a request handler responsible for handling a request. Parse the * @a upload_data (if applicable) and the @a url and call the * handler. * * @param rc request context * @param url rest of the URL to parse * @param upload_data upload data to parse (if available) * @param[in,out] upload_data_size number of bytes in @a upload_data * @return MHD result code */ static MHD_RESULT proceed_with_handler (struct TEKT_RequestContext *rc, const char *url, const char *upload_data, size_t *upload_data_size) { const struct TEKT_RequestHandler *rh = rc->rh; const char *args[rh->nargs + 2]; size_t ulen = strlen (url) + 1; MHD_RESULT ret; /* We do check for "ulen" here, because we'll later stack-allocate a buffer of that size and don't want to enable malicious clients to cause us huge stack allocations. */ if (ulen > 512) { /* 512 is simply "big enough", as it is bigger than "6 * 54", which is the longest URL format we ever get (for /deposits/). The value should be adjusted if we ever define protocol endpoints with plausibly longer inputs. */ GNUNET_break_op (0); return TALER_MHD_reply_with_error ( rc->connection, MHD_HTTP_URI_TOO_LONG, TALER_EC_GENERIC_URI_TOO_LONG, url); } /* All POST endpoints come with a body in JSON format. So we parse the JSON here. */ if ( (NULL == rc->root) && (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_POST)) ) { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_post_json ( rc->connection, &rc->opaque_post_parsing_context, upload_data, upload_data_size, &rc->root); if (GNUNET_SYSERR == res) { GNUNET_assert (NULL == rc->root); GNUNET_break (0); return MHD_NO; /* bad upload, could not even generate error */ } if ( (GNUNET_NO == res) || (NULL == rc->root) ) { GNUNET_assert (NULL == rc->root); return MHD_YES; /* so far incomplete upload or parser error */ } } { char d[ulen]; unsigned int i; char *sp; /* Parse command-line arguments */ /* make a copy of 'url' because 'strtok_r()' will modify */ GNUNET_memcpy (d, url, ulen); i = 0; args[i++] = strtok_r (d, "/", &sp); while ( (NULL != args[i - 1]) && (i <= rh->nargs + 1) ) args[i++] = strtok_r (NULL, "/", &sp); /* make sure above loop ran nicely until completion, and also that there is no excess data in 'd' afterwards */ if ( ( (rh->nargs_is_upper_bound) && (i - 1 > rh->nargs) ) || ( (! rh->nargs_is_upper_bound) && (i - 1 != rh->nargs) ) ) { char emsg[128 + 512]; GNUNET_snprintf (emsg, sizeof (emsg), "Got %u+/%u segments for `%s' request (`%s')", i - 1, rh->nargs, rh->url, url); GNUNET_break_op (0); return TALER_MHD_reply_with_error ( rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS, emsg); } GNUNET_assert (NULL == args[i - 1]); /* Above logic ensures that 'root' is exactly non-NULL for POST operations, so we test for 'root' to decide which handler to invoke. */ if (NULL != rc->root) ret = rh->handler.post (rc, rc->root, args); else /* We also only have "POST" or "GET" in the API for at this point (OPTIONS/HEAD are taken care of earlier) */ ret = rh->handler.get (rc, args); } return ret; } static void rh_cleaner_cb (struct TEKT_RequestContext *rc) { if (NULL != rc->response) { MHD_destroy_response (rc->response); rc->response = NULL; } if (NULL != rc->root) { json_decref (rc->root); rc->root = NULL; } } /** * Handle incoming HTTP request. * * @param cls closure for MHD daemon (unused) * @param connection the connection * @param url the requested url * @param method the method (POST, GET, ...) * @param version HTTP version (ignored) * @param upload_data request data * @param upload_data_size size of @a upload_data in bytes * @param con_cls closure for request (a `struct TEKT_RequestContext *`) * @return MHD result code */ static MHD_RESULT handle_mhd_request (void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { static struct TEKT_RequestHandler handlers[] = { /* simulated KYC endpoints */ { .url = "kyc-proof", .method = MHD_HTTP_METHOD_GET, .handler.get = &handler_kyc_proof_get, .nargs = 1 }, { .url = "kyc-webhook", .method = MHD_HTTP_METHOD_POST, .handler.post = &handler_kyc_webhook_post, .nargs = 128, .nargs_is_upper_bound = true }, { .url = "kyc-webhook", .method = MHD_HTTP_METHOD_GET, .handler.get = &handler_kyc_webhook_get, .nargs = 128, .nargs_is_upper_bound = true }, /* mark end of list */ { .url = NULL } }; struct TEKT_RequestContext *rc = *con_cls; (void) cls; (void) version; if (NULL == rc) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling new request\n"); /* We're in a new async scope! */ rc = *con_cls = GNUNET_new (struct TEKT_RequestContext); rc->url = url; rc->connection = connection; rc->rh_cleaner = &rh_cleaner_cb; } if (NULL != rc->response) { return MHD_queue_response (rc->connection, rc->http_status, rc->response); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling request (%s) for URL '%s'\n", method, url); /* on repeated requests, check our cache first */ if (NULL != rc->rh) { const char *start; if ('\0' == url[0]) /* strange, should start with '/', treat as just "/" */ url = "/"; start = strchr (url + 1, '/'); if (NULL == start) start = ""; return proceed_with_handler (rc, start, upload_data, upload_data_size); } if (0 == strcasecmp (method, MHD_HTTP_METHOD_HEAD)) method = MHD_HTTP_METHOD_GET; /* treat HEAD as GET here, MHD will do the rest */ /* parse first part of URL */ { bool found = false; size_t tok_size; const char *tok; const char *rest; if ('\0' == url[0]) /* strange, should start with '/', treat as just "/" */ url = "/"; tok = url + 1; rest = strchr (tok, '/'); if (NULL == rest) { tok_size = strlen (tok); } else { tok_size = rest - tok; rest++; /* skip over '/' */ } for (unsigned int i = 0; NULL != handlers[i].url; i++) { struct TEKT_RequestHandler *rh = &handlers[i]; if ( (0 != strncmp (tok, rh->url, tok_size)) || (tok_size != strlen (rh->url) ) ) continue; found = true; /* The URL is a match! What we now do depends on the method. */ if (0 == strcasecmp (method, MHD_HTTP_METHOD_OPTIONS)) { return TALER_MHD_reply_cors_preflight (connection); } GNUNET_assert (NULL != rh->method); if (0 != strcasecmp (method, rh->method)) { found = true; continue; } /* cache to avoid the loop next time */ rc->rh = rh; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handler found for %s '%s'\n", method, url); return MHD_YES; } if (found) { /* we found a matching address, but the method is wrong */ struct MHD_Response *reply; MHD_RESULT ret; char *allowed = NULL; GNUNET_break_op (0); for (unsigned int i = 0; NULL != handlers[i].url; i++) { struct TEKT_RequestHandler *rh = &handlers[i]; if ( (0 != strncmp (tok, rh->url, tok_size)) || (tok_size != strlen (rh->url) ) ) continue; if (NULL == allowed) { allowed = GNUNET_strdup (rh->method); } else { char *tmp; GNUNET_asprintf (&tmp, "%s, %s", allowed, rh->method); GNUNET_free (allowed); allowed = tmp; } if (0 == strcasecmp (rh->method, MHD_HTTP_METHOD_GET)) { char *tmp; GNUNET_asprintf (&tmp, "%s, %s", allowed, MHD_HTTP_METHOD_HEAD); GNUNET_free (allowed); allowed = tmp; } } reply = TALER_MHD_make_error ( TALER_EC_GENERIC_METHOD_INVALID, method); GNUNET_break ( MHD_YES == MHD_add_response_header (reply, MHD_HTTP_HEADER_ALLOW, allowed)); GNUNET_free (allowed); ret = MHD_queue_response (connection, MHD_HTTP_METHOD_NOT_ALLOWED, reply); MHD_destroy_response (reply); return ret; } } /* No handler matches, generate not found */ return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, url); } /** * Load configuration parameters for the exchange * server into the corresponding global variables. * * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue exchange_serve_process_config (void) { if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (TEKT_cfg, "exchange", "BASE_URL", &TEKT_base_url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "exchange", "BASE_URL"); return GNUNET_SYSERR; } if (! TALER_url_valid_charset (TEKT_base_url)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, "exchange", "BASE_URL", "invalid URL"); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Function run on shutdown. * * @param cls NULL */ static void do_shutdown (void *cls) { struct MHD_Daemon *mhd; struct ProofRequestState *rs; (void) cls; while (NULL != (rs = rs_head)) { GNUNET_CONTAINER_DLL_remove (rs_head, rs_tail, rs); rs->logic->proof_cancel (rs->ph); MHD_resume_connection (rs->rc->connection); GNUNET_free (rs); } if (NULL != ih) { ih_logic->initiate_cancel (ih); ih = NULL; } kyc_webhook_cleanup (); TALER_KYCLOGIC_kyc_done (); mhd = TALER_MHD_daemon_stop (); if (NULL != mhd) MHD_stop_daemon (mhd); if (NULL != TEKT_curl_ctx) { GNUNET_CURL_fini (TEKT_curl_ctx); TEKT_curl_ctx = NULL; } if (NULL != exchange_curl_rc) { GNUNET_CURL_gnunet_rc_destroy (exchange_curl_rc); exchange_curl_rc = NULL; } TALER_TEMPLATING_done (); } /** * Function called with the result of a KYC initiation * operation. * * @param cls closure * @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) { (void) cls; ih = NULL; if (TALER_EC_NONE != ec) { fprintf (stderr, "Failed to start KYC process: %s (#%d)\n", error_msg_hint, ec); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } { char *s; s = GNUNET_STRINGS_data_to_string_alloc (&cmd_line_h_payto, sizeof (cmd_line_h_payto)); if (NULL != provider_user_id) { fprintf (stdout, "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -u '%s' -U '%s' -p %s\n", redirect_url, provider_user_id, provider_legitimization_id, s); } else { fprintf (stdout, "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -U '%s' -p %s\n", redirect_url, provider_legitimization_id, s); } GNUNET_free (s); } GNUNET_free (cmd_provider_user_id); GNUNET_free (cmd_provider_legitimization_id); if (NULL != provider_user_id) cmd_provider_user_id = GNUNET_strdup (provider_user_id); if (NULL != provider_legitimization_id) cmd_provider_legitimization_id = GNUNET_strdup (provider_legitimization_id); if (! run_webservice) GNUNET_SCHEDULER_shutdown (); } /** * Function called to iterate over KYC-relevant * transaction amounts for a particular time range. * Called within a database transaction, so must * not start a new one. * * @param cls closure, identifies the event type and * account to iterate over events for * @param limit maximum time-range for which events * should be fetched (timestamp in the past) * @param cb function to call on each event found, * events must be returned in reverse chronological * order * @param cb_cls closure for @a cb * @return transaction status */ static enum GNUNET_DB_QueryStatus amount_iterator ( void *cls, struct GNUNET_TIME_Absolute limit, TALER_EXCHANGEDB_KycAmountCallback cb, void *cb_cls) { const struct TALER_Amount *amount = cls; struct GNUNET_TIME_Absolute date; enum GNUNET_GenericReturnValue ret; date = GNUNET_TIME_absolute_subtract (limit, GNUNET_TIME_UNIT_SECONDS); ret = cb (cb_cls, amount, date); GNUNET_break (GNUNET_SYSERR != ret); if (GNUNET_OK != ret) return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } /** * Main function that will be run by the scheduler. * * @param cls closure * @param args remaining command-line arguments * @param cfgfile name of the configuration file used (for saving, can be * NULL!) * @param config configuration */ static void run (void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *config) { enum TALER_KYCLOGIC_KycTriggerEvent event; struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs = NULL; const struct TALER_KYCLOGIC_KycRule *rule = NULL; int fh; (void) cls; (void) args; (void ) cfgfile; if (GNUNET_OK != TALER_TEMPLATING_init ("exchange")) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not load templates. Installation broken.\n"); global_ret = EXIT_FAILURE; return; } if (NULL != operation_s) { if (GNUNET_OK != TALER_KYCLOGIC_kyc_trigger_from_string (operation_s, &event)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Malformed operation type `%s'\n", operation_s); global_ret = EXIT_FAILURE; return; } } if (NULL != lrs_s) { json_t *jlrs; json_error_t err; jlrs = json_loads (lrs_s, JSON_REJECT_DUPLICATES, &err); if (NULL == jlrs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Malformed JSON for legitimization rule set: %s at %d\n", err.text, err.position); global_ret = EXIT_INVALIDARGUMENT; return; } lrs = TALER_KYCLOGIC_rules_parse (jlrs); json_decref (jlrs); if (NULL == lrs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Malformed legitimization rule set `%s'\n", lrs_s); global_ret = EXIT_INVALIDARGUMENT; return; } } if (print_h_payto) { char *s; s = GNUNET_STRINGS_data_to_string_alloc (&cmd_line_h_payto, sizeof (cmd_line_h_payto)); fprintf (stdout, "%s\n", s); GNUNET_free (s); } TALER_MHD_setup (TALER_MHD_GO_NONE); TEKT_cfg = config; GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); if (GNUNET_OK != TALER_KYCLOGIC_kyc_init (config)) { global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != exchange_serve_process_config ()) { global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } global_ret = EXIT_SUCCESS; if (NULL != operation_s) { enum GNUNET_DB_QueryStatus qs; if (GNUNET_OK == TALER_amount_is_valid (&trigger_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Trigger amount command-line option (-t) required\n"); global_ret = EXIT_INVALIDARGUMENT; GNUNET_SCHEDULER_shutdown (); return; } { struct TALER_Amount next_threshold; qs = TALER_KYCLOGIC_kyc_test_required ( event, lrs, &amount_iterator, &trigger_amount, &rule, &next_threshold); } switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: fprintf (stdout, "KYC not required for the given operation type and amount\n"); global_ret = EXIT_SUCCESS; GNUNET_SCHEDULER_shutdown (); return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } } if (NULL != rule) { struct TALER_KYCLOGIC_KycCheckContext kcc; if (0 != list_measures) { // FIXME: print rule with possible measures! GNUNET_break (0); global_ret = EXIT_SUCCESS; GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != TALER_KYCLOGIC_requirements_to_check (lrs, rule, measure, &kcc)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not initiate KYC for measure `%s' (configuration error?)\n", measure); global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } if (NULL == kcc.check) { GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "SKIP check selected, nothing to do here\n"); global_ret = EXIT_SUCCESS; GNUNET_SCHEDULER_shutdown (); return; } switch (kcc.check->type) { case TALER_KYCLOGIC_CT_INFO: GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "KYC information is `%s'\n", kcc.check->description); break; case TALER_KYCLOGIC_CT_FORM: GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, "Would initiate KYC check `%s' with form `%s'\n", kcc.check->check_name, kcc.check->details.form.name); break; case TALER_KYCLOGIC_CT_LINK: { struct TALER_KYCLOGIC_ProviderDetails *pd; const char *provider_name; TALER_KYCLOGIC_provider_to_logic ( kcc.check->details.link.provider, &ih_logic, &pd, &provider_name); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Initiating KYC check `%s' at provider `%s'\n", kcc.check->check_name, provider_name); ih = ih_logic->initiate (ih_logic->cls, pd, &cmd_line_h_payto, kyc_row_id, &initiate_cb, NULL); GNUNET_break (NULL != ih); break; } } } if (run_webservice) { TEKT_curl_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &exchange_curl_rc); if (NULL == TEKT_curl_ctx) { GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } exchange_curl_rc = GNUNET_CURL_gnunet_rc_create (TEKT_curl_ctx); fh = TALER_MHD_bind (TEKT_cfg, "exchange", &serve_port); if ( (0 == serve_port) && (-1 == fh) ) { GNUNET_SCHEDULER_shutdown (); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting daemon on port %u\n", (unsigned int) serve_port); mhd = MHD_start_daemon ( MHD_USE_SUSPEND_RESUME | MHD_USE_PIPE_FOR_SHUTDOWN | MHD_USE_DEBUG | MHD_USE_DUAL_STACK | MHD_USE_TCP_FASTOPEN, (-1 == fh) ? serve_port : 0, NULL, NULL, &handle_mhd_request, NULL, MHD_OPTION_LISTEN_SOCKET, fh, MHD_OPTION_EXTERNAL_LOGGER, &TALER_MHD_handle_logs, NULL, MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, NULL, MHD_OPTION_END); if (NULL == mhd) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to launch HTTP service. Is the port in use?\n"); GNUNET_SCHEDULER_shutdown (); return; } TALER_MHD_daemon_start (mhd); } } /** * The main function of the taler-exchange-httpd server ("the exchange"). * * @param argc number of arguments from the command line * @param argv command line arguments * @return 0 ok, 1 on error */ int main (int argc, char *const *argv) { const struct GNUNET_GETOPT_CommandLineOption options[] = { GNUNET_GETOPT_option_help ( "tool to test KYC provider integrations"), GNUNET_GETOPT_option_flag ( 'M', "list-measures", "list available measures", &list_measures), GNUNET_GETOPT_option_string ( 'm', "measure", "MEASURE_NAME", "initiate KYC check for the selected measure", &measure), GNUNET_GETOPT_option_string ( 'o', "operation", "OPERATION_TYPE", "name of the operation that triggers legitimization (WITHDRAW, DEPOSIT, etc.)", &operation_s), GNUNET_GETOPT_option_flag ( 'P', "print-payto-hash", "output the hash of the payto://-URI", &print_h_payto), GNUNET_GETOPT_option_base32_fixed_size ( 'p', "payto-hash", "HASH", "base32 encoding of the hash of a payto://-URI to use for the account (otherwise a random value will be used)", &cmd_line_h_payto, sizeof (cmd_line_h_payto)), GNUNET_GETOPT_option_string ( 'R', "ruleset", "JSON", "use the given legitimization rule set (otherwise defaults from configuration are used)", &lrs_s), GNUNET_GETOPT_option_uint ( 'r', "rowid", "NUMBER", "override row ID to use in simulation (default: 42)", &kyc_row_id), TALER_getopt_get_amount ( 't', "trigger", "AMOUNT", "threshold crossed that would trigger some legitimization rule", &trigger_amount), GNUNET_GETOPT_option_string ( 'U', "legitimization", "ID", "use the given provider legitimization ID (overridden if -i is also used)", &cmd_provider_legitimization_id), GNUNET_GETOPT_option_string ( 'u', "user", "ID", "use the given provider user ID (overridden if -i is also used)", &cmd_provider_user_id), GNUNET_GETOPT_option_flag ( 'w', "run-webservice", "run the integrated HTTP service", &run_webservice), GNUNET_GETOPT_OPTION_END }; enum GNUNET_GenericReturnValue ret; TALER_OS_init (); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &cmd_line_h_payto, sizeof (cmd_line_h_payto)); ret = GNUNET_PROGRAM_run (argc, argv, "taler-exchange-kyc-tester", "tool to test KYC provider integrations", options, &run, NULL); if (GNUNET_SYSERR == ret) return EXIT_INVALIDARGUMENT; if (GNUNET_NO == ret) return EXIT_SUCCESS; return global_ret; } /* end of taler-exchange-kyc-tester.c */