diff options
author | Christian Grothoff <christian@grothoff.org> | 2022-08-08 15:22:45 +0200 |
---|---|---|
committer | Christian Grothoff <christian@grothoff.org> | 2022-08-08 15:22:45 +0200 |
commit | ff48ada7d5ba026a39c0e8ddefd54b80b813425b (patch) | |
tree | 14efbb57f75329c7d818b766b07d2b80c38fd35c /src/kyclogic | |
parent | d4fb0695ceb2f1c1ceb0ef1fc6fdded668396d4d (diff) | |
download | exchange-ff48ada7d5ba026a39c0e8ddefd54b80b813425b.tar.xz |
move kyclogic into libtalerkyclogic
Diffstat (limited to 'src/kyclogic')
-rw-r--r-- | src/kyclogic/Makefile.am | 12 | ||||
-rw-r--r-- | src/kyclogic/kyclogic_api.c | 1062 | ||||
-rw-r--r-- | src/kyclogic/taler-exchange-kyc-tester.c | 347 |
3 files changed, 1414 insertions, 7 deletions
diff --git a/src/kyclogic/Makefile.am b/src/kyclogic/Makefile.am index 7c754a806..c57fc1fce 100644 --- a/src/kyclogic/Makefile.am +++ b/src/kyclogic/Makefile.am @@ -16,6 +16,17 @@ EXTRA_DIST = \ kyclogic.conf \ kyclogic-oauth2.conf +lib_LTLIBRARIES = \ + libtalerkyclogic.la + +libtalerkyclogic_la_SOURCES = \ + kyclogic_api.c +libtalerkyclogic_la_LIBADD = \ + $(top_builddir)/src/util/libtalerutil.la \ + -lgnunetutil \ + $(XLIB) + + bin_PROGRAMS = \ taler-exchange-kyc-tester @@ -23,6 +34,7 @@ taler_exchange_kyc_tester_SOURCES = \ taler-exchange-kyc-tester.c taler_exchange_kyc_tester_LDADD = \ $(LIBGCRYPT_LIBS) \ + libtalerkyclogic.la \ $(top_builddir)/src/mhd/libtalermhd.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/util/libtalerutil.la \ diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c new file mode 100644 index 000000000..f2d31acf5 --- /dev/null +++ b/src/kyclogic/kyclogic_api.c @@ -0,0 +1,1062 @@ +/* + This file is part of TALER + Copyright (C) 2022 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file kyclogic_api.c + * @brief server-side KYC API + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_kyclogic_lib.h" + +/** + * Information about a KYC provider. + */ +struct TALER_KYCLOGIC_KycProvider; + + +/** + * Abstract representation of a KYC check. + */ +struct TALER_KYCLOGIC_KycCheck +{ + /** + * Human-readable name given to the KYC check. + */ + char *name; + + /** + * Array of @e num_providers providers that offer this type of KYC check. + */ + struct TALER_KYCLOGIC_KycProvider **providers; + + /** + * Length of the @e providers array. + */ + unsigned int num_providers; + +}; + + +struct TALER_KYCLOGIC_KycProvider +{ + /** + * Name of the provider (configuration section name). + */ + const char *provider_section_name; + + /** + * Array of @e num_checks checks performed by this provider. + */ + struct TALER_KYCLOGIC_KycCheck **provided_checks; + + /** + * Logic to run for this provider. + */ + struct TALER_KYCLOGIC_Plugin *logic; + + /** + * @e provider_section_name specific details to + * pass to the @e logic functions. + */ + struct TALER_KYCLOGIC_ProviderDetails *pd; + + /** + * Cost of running this provider's KYC. + */ + unsigned long long cost; + + /** + * Length of the @e checks array. + */ + unsigned int num_checks; + + /** + * Type of user this provider supports. + */ + enum TALER_KYCLOGIC_KycUserType user_type; +}; + + +/** + * Condition that triggers a need to perform KYC. + */ +struct TALER_KYCLOGIC_KycTrigger +{ + + /** + * Timeframe to consider for computing the amount + * to compare against the @e limit. Zero for the + * wallet balance trigger (as not applicable). + */ + struct GNUNET_TIME_Relative timeframe; + + /** + * Maximum amount that can be transacted until + * the rule triggers. + */ + struct TALER_Amount threshold; + + /** + * Array of @e num_checks checks to apply on this trigger. + */ + struct TALER_KYCLOGIC_KycCheck **required_checks; + + /** + * Length of the @e checks array. + */ + unsigned int num_checks; + + /** + * What event is this trigger for? + */ + enum TALER_KYCLOGIC_KycTriggerEvent trigger; + +}; + + +/** + * Array of @e num_kyc_logics KYC logic plugins we have loaded. + */ +static struct TALER_KYCLOGIC_Plugin **kyc_logics; + +/** + * Length of the #kyc_logics array. + */ +static unsigned int num_kyc_logics; + +/** + * Array of @e num_kyc_checks known types of + * KYC checks. + */ +static struct TALER_KYCLOGIC_KycCheck **kyc_checks; + +/** + * Length of the #kyc_checks array. + */ +static unsigned int num_kyc_checks; + +/** + * Array of configured triggers. + */ +static struct TALER_KYCLOGIC_KycTrigger **kyc_triggers; + +/** + * Length of the #kyc_triggers array. + */ +static unsigned int num_kyc_triggers; + +/** + * Array of configured providers. + */ +static struct TALER_KYCLOGIC_KycProvider **kyc_providers; + +/** + * Length of the #kyc_providers array. + */ +static unsigned int num_kyc_providers; + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s, + enum TALER_KYCLOGIC_KycTriggerEvent * + trigger) +{ + struct + { + const char *in; + enum TALER_KYCLOGIC_KycTriggerEvent out; + } map [] = { + { "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW }, + { "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT }, + { "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE }, + { "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE }, + { NULL, 0 } + }; + + for (unsigned int i = 0; NULL != map[i].in; i++) + if (0 == strcasecmp (map[i].in, + trigger_s)) + { + *trigger = map[i].out; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid KYC trigger `%s'\n", + trigger_s); + return GNUNET_SYSERR; +} + + +const char * +TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger) +{ + switch (trigger) + { + case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW: + return "withdraw"; + case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT: + return "deposit"; + case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE: + return "merge"; + case TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE: + return "balance"; + } + GNUNET_break (0); + return NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_user_type_from_string (const char *ut_s, + enum TALER_KYCLOGIC_KycUserType *ut) +{ + struct + { + const char *in; + enum TALER_KYCLOGIC_KycTriggerEvent out; + } map [] = { + { "individual", TALER_KYCLOGIC_KYC_UT_INDIVIDUAL }, + { "business", TALER_KYCLOGIC_KYC_UT_BUSINESS }, + { NULL, 0 } + }; + + for (unsigned int i = 0; NULL != map[i].in; i++) + if (0 == strcasecmp (map[i].in, + ut_s)) + { + *ut = map[i].out; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid user type `%s'\n", + ut_s); + return GNUNET_SYSERR; +} + + +const char * +TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) +{ + switch (ut) + { + case TALER_KYCLOGIC_KYC_UT_INDIVIDUAL: + return "individual"; + case TALER_KYCLOGIC_KYC_UT_BUSINESS: + return "business"; + } + GNUNET_break (0); + return NULL; +} + + +/** + * Load KYC logic plugin. + * + * @param cfg configuration to use + * @param name name of the plugin + * @return NULL on error + */ +static struct TALER_KYCLOGIC_Plugin * +load_logic (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *name) +{ + char *lib_name; + struct TALER_KYCLOGIC_Plugin *plugin; + + GNUNET_asprintf (&lib_name, + "libtaler_plugin_kyclogic_%s", + name); + plugin = GNUNET_PLUGIN_load (lib_name, + (void *) cfg); + if (NULL != plugin) + plugin->library_name = lib_name; + else + GNUNET_free (lib_name); + return plugin; +} + + +/** + * Add check type to global array of checks. + * First checks if the type already exists, otherwise + * adds a new one. + * + * @param check name of the check + * @return pointer into the global list + */ +static struct TALER_KYCLOGIC_KycCheck * +add_check (const char *check) +{ + struct TALER_KYCLOGIC_KycCheck *kc; + + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == strcasecmp (check, + kyc_checks[i]->name)) + return kyc_checks[i]; + kc = GNUNET_new (struct TALER_KYCLOGIC_KycCheck); + kc->name = GNUNET_strdup (check); + GNUNET_array_append (kyc_checks, + num_kyc_checks, + kc); + return kc; +} + + +/** + * Parse list of checks from @a checks and build an + * array of aliases into the global checks array + * in @a provided_checks. + * + * @param[in,out] checks list of checks; clobbered + * @param[out] p_checks where to put array of aliases + * @param[out] num_p_checks set to length of @a p_checks array + */ +static void +add_checks (char *checks, + struct TALER_KYCLOGIC_KycCheck ***p_checks, + unsigned int *num_p_checks) +{ + char *sptr; + struct TALER_KYCLOGIC_KycCheck **rchecks = NULL; + unsigned int num_rchecks = 0; + + for (char *tok = strtok_r (checks, " ", &sptr); + NULL != tok; + tok = strtok_r (NULL, " ", &sptr)) + { + struct TALER_KYCLOGIC_KycCheck *kc; + + kc = add_check (tok); + GNUNET_array_append (rchecks, + num_rchecks, + kc); + } + *p_checks = rchecks; + *num_p_checks = num_rchecks; +} + + +/** + * Parse configuration of a KYC provider. + * + * @param cfg configuration to parse + * @param section name of the section to analyze + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section) +{ + unsigned long long cost; + char *logic; + char *ut_s; + enum TALER_KYCLOGIC_KycUserType ut; + char *checks; + struct TALER_KYCLOGIC_Plugin *lp; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (cfg, + section, + "COST", + &cost)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "COST", + "number required"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "USER_TYPE", + &ut_s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "USER_TYPE"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_user_type_from_string (ut_s, + &ut)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "USER_TYPE", + "valid user type required"); + GNUNET_free (ut_s); + return GNUNET_SYSERR; + } + GNUNET_free (ut_s); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "LOGIC", + &logic)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "LOGIC"); + return GNUNET_SYSERR; + } + lp = load_logic (cfg, + logic); + if (NULL == lp) + { + GNUNET_free (logic); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "LOGIC", + "logic plugin could not be loaded"); + return GNUNET_SYSERR; + } + GNUNET_free (logic); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "PROVIDED_CHECKS", + &checks)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "PROVIDED_CHECKS"); + return GNUNET_SYSERR; + } + { + struct TALER_KYCLOGIC_KycProvider *kp; + + kp = GNUNET_new (struct TALER_KYCLOGIC_KycProvider); + kp->provider_section_name = section; + kp->user_type = ut; + kp->logic = lp; + kp->cost = cost; + add_checks (checks, + &kp->provided_checks, + &kp->num_checks); + GNUNET_free (checks); + kp->pd = lp->load_configuration (lp->cls, + section); + if (NULL == kp->pd) + { + GNUNET_free (kp); + return GNUNET_SYSERR; + } + GNUNET_array_append (kyc_providers, + num_kyc_providers, + kp); + for (unsigned int i = 0; i<kp->num_checks; i++) + { + struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[i]; + + GNUNET_array_append (kc->providers, + kc->num_providers, + kp); + } + } + return GNUNET_OK; +} + + +static enum GNUNET_GenericReturnValue +add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, + const char *section) +{ + char *ot_s; + struct TALER_Amount threshold; + struct GNUNET_TIME_Relative timeframe; + char *checks; + enum TALER_KYCLOGIC_KycTriggerEvent ot; + + if (GNUNET_OK != + TALER_config_get_amount (cfg, + section, + "THRESHOLD", + &threshold)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "THRESHOLD", + "amount required"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "OPERATION_TYPE", + &ot_s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "OPERATION_TYPE"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_trigger_from_string (ot_s, + &ot)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "OPERATION_TYPE", + "valid trigger type required"); + GNUNET_free (ot_s); + return GNUNET_SYSERR; + } + GNUNET_free (ot_s); + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + section, + "TIMEFRAME", + &timeframe)) + { + if (TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE == ot) + { + timeframe = GNUNET_TIME_UNIT_ZERO; + } + else + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "TIMEFRAME", + "duration required"); + return GNUNET_SYSERR; + } + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "REQUIRED_CHECKS", + &checks)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "REQUIRED_CHECKS"); + return GNUNET_SYSERR; + } + + { + struct TALER_KYCLOGIC_KycTrigger *kt; + + kt = GNUNET_new (struct TALER_KYCLOGIC_KycTrigger); + kt->timeframe = timeframe; + kt->threshold = threshold; + kt->trigger = ot; + add_checks (checks, + &kt->required_checks, + &kt->num_checks); + GNUNET_free (checks); + GNUNET_array_append (kyc_triggers, + num_kyc_triggers, + kt); + } + return GNUNET_OK; +} + + +/** + * Closure for #handle_section(). + */ +struct SectionContext +{ + /** + * Configuration to handle. + */ + const struct GNUNET_CONFIGURATION_Handle *cfg; + + /** + * Result to return, set to false on failures. + */ + bool result; +}; + + +/** + * Function to iterate over configuration sections. + * + * @param cls a `struct SectionContext *` + * @param section name of the section + */ +static void +handle_section (void *cls, + const char *section) +{ + struct SectionContext *sc = cls; + + if (0 == strncasecmp (section, + "kyc-provider-", + strlen ("kyc-provider-"))) + { + if (GNUNET_OK != + add_provider (sc->cfg, + section)) + sc->result = false; + return; + } + if (0 == strncasecmp (section, + "kyc-legitimization-", + strlen ("kyc-legitimization-"))) + { + if (GNUNET_OK != + add_trigger (sc->cfg, + section)) + sc->result = false; + return; + } +} + + +/** + * Comparator for qsort. Compares two triggers + * by timeframe to sort triggers by time. + * + * @param p1 first trigger to compare + * @param p2 second trigger to compare + * @return -1 if p1 < p2, 0 if p1==p2, 1 if p1 > p2. + */ +static int +sort_by_timeframe (const void *p1, + const void *p2) +{ + struct TALER_KYCLOGIC_KycTrigger **t1 = (struct + TALER_KYCLOGIC_KycTrigger **) p1; + struct TALER_KYCLOGIC_KycTrigger **t2 = (struct + TALER_KYCLOGIC_KycTrigger **) p2; + + if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, + <, + (*t2)->timeframe)) + return -1; + if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, + >, + (*t2)->timeframe)) + return 1; + return 0; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + struct SectionContext sc = { + .cfg = cfg, + .result = true + }; + + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_section, + &sc); + if (! sc.result) + { + TALER_KYCLOGIC_kyc_done (); + return GNUNET_SYSERR; + } + + /* sanity check: ensure at least one provider exists + for any trigger and indidivual or business. */ + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == kyc_checks[i]->num_providers) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No provider available for required KYC check `%s'\n", + kyc_checks[i]->name); + TALER_KYCLOGIC_kyc_done (); + return GNUNET_SYSERR; + } + qsort (kyc_triggers, + num_kyc_triggers, + sizeof (struct TALER_KYCLOGIC_KycTrigger *), + &sort_by_timeframe); + return GNUNET_OK; +} + + +void +TALER_KYCLOGIC_kyc_done (void) +{ + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + GNUNET_array_grow (kt->required_checks, + kt->num_checks, + 0); + GNUNET_free (kt); + } + GNUNET_array_grow (kyc_triggers, + num_kyc_triggers, + 0); + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + kp->logic->unload_configuration (kp->pd); + GNUNET_array_grow (kp->provided_checks, + kp->num_checks, + 0); + GNUNET_free (kp); + } + GNUNET_array_grow (kyc_providers, + num_kyc_providers, + 0); + for (unsigned int i = 0; i<num_kyc_logics; i++) + { + struct TALER_KYCLOGIC_Plugin *lp = kyc_logics[i]; + char *lib_name = lp->library_name; + + GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, + lp)); + GNUNET_free (lib_name); + } + GNUNET_array_grow (kyc_logics, + num_kyc_logics, + 0); + for (unsigned int i = 0; i<num_kyc_checks; i++) + { + struct TALER_KYCLOGIC_KycCheck *kc = kyc_checks[i]; + + GNUNET_free (kc->name); + GNUNET_free (kc); + } + GNUNET_array_grow (kyc_checks, + num_kyc_checks, + 0); +} + + +/** + * Closure for the #eval_trigger(). + */ +struct ThresholdTestContext +{ + /** + * Total amount so far. + */ + struct TALER_Amount total; + + /** + * Trigger event to evaluate triggers of. + */ + enum TALER_KYCLOGIC_KycTriggerEvent event; + + /** + * Offset in the triggers array where we need to start + * checking for triggers. All trigges below this + * offset were already hit. + */ + unsigned int start; + + /** + * Array of checks needed so far. + */ + struct TALER_KYCLOGIC_KycCheck **needed; + + /** + * Pointer to number of entries used in @a needed. + */ + unsigned int *needed_cnt; + + /** + * Has @e total been initialized yet? + */ + bool have_total; +}; + + +/** + * Function called on each @a amount that was found to + * be relevant for a KYC check. + * + * @param cls closure to allow the KYC module to + * total up amounts and evaluate rules + * @param amount encountered transaction amount + * @param date when was the amount encountered + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to abort iteration + * #GNUNET_SYSERR on internal error (also abort itaration) + */ +static enum GNUNET_GenericReturnValue +eval_trigger (void *cls, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Absolute date) +{ + struct ThresholdTestContext *ttc = cls; + struct GNUNET_TIME_Relative duration; + bool bump = true; + + duration = GNUNET_TIME_absolute_get_duration (date); + if (ttc->have_total) + { + if (0 > + TALER_amount_add (&ttc->total, + &ttc->total, + amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + else + { + ttc->total = *amount; + } + for (unsigned int i = ttc->start; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (ttc->event != kt->trigger) + continue; + duration = GNUNET_TIME_relative_max (duration, + kt->timeframe); + if (GNUNET_TIME_relative_cmp (kt->timeframe, + >, + duration)) + { + if (bump) + ttc->start = i; + return GNUNET_OK; + } + if (-1 == + TALER_amount_cmp (&ttc->total, + &kt->threshold)) + { + if (bump) + ttc->start = i; + bump = false; + continue; /* amount too low to trigger */ + } + /* add check to list of required checks, unless + already present... */ + for (unsigned int j = 0; j<kt->num_checks; j++) + { + struct TALER_KYCLOGIC_KycCheck *rc = kt->required_checks[j]; + bool found = false; + + for (unsigned int k = 0; k<*ttc->needed_cnt; k++) + if (ttc->needed[k] == rc) + { + found = true; + break; + } + if (! found) + { + ttc->needed[*ttc->needed_cnt] = rc; + (*ttc->needed_cnt)++; + } + } + } + if (bump) + return GNUNET_NO; /* we hit all possible triggers! */ + return GNUNET_OK; +} + + +/** + * Closure for the #remove_satisfied(). + */ +struct RemoveContext +{ + + /** + * Array of checks needed so far. + */ + struct TALER_KYCLOGIC_KycCheck **needed; + + /** + * Pointer to number of entries used in @a needed. + */ + unsigned int *needed_cnt; + +}; + + +/** + * Remove all checks satisfied by @a provider_name from + * our list of checks. + * + * @param cls a `struct RemoveContext` + * @param provider_name section name of provider that was already run previously + */ +static void +remove_satisfied (void *cls, + const char *provider_name) +{ + struct RemoveContext *rc = cls; + + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != strcasecmp (provider_name, + kp->provider_section_name)) + continue; + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + for (unsigned int k = 0; k<*rc->needed_cnt; k++) + if (kc == rc->needed[k]) + { + rc->needed[k] = rc->needed[*rc->needed_cnt - 1]; + (*rc->needed_cnt)--; + if (0 == *rc->needed_cnt) + return; /* for sure finished */ + break; + } + } + break; + } +} + + +const char * +TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, + const struct TALER_PaytoHashP *h_payto, + TALER_KYCLOGIC_KycSatisfiedIterator ki, + void *ki_cls, + TALER_KYCLOGIC_KycAmountIterator ai, + void *ai_cls) +{ + struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; + unsigned int needed_cnt = 0; + struct GNUNET_TIME_Relative timeframe; + unsigned long long min_cost = ULONG_LONG_MAX; + unsigned int max_checks = 0; + const struct TALER_KYCLOGIC_KycProvider *kp_best = NULL; + + timeframe = GNUNET_TIME_UNIT_ZERO; + for (unsigned int i = 0; i<num_kyc_triggers; i++) + { + const struct TALER_KYCLOGIC_KycTrigger *kt = kyc_triggers[i]; + + if (event != kt->trigger) + continue; + timeframe = GNUNET_TIME_relative_max (timeframe, + kt->timeframe); + } + { + struct GNUNET_TIME_Absolute now; + struct ThresholdTestContext ttc = { + .event = event, + .needed = needed, + .needed_cnt = &needed_cnt + }; + + now = GNUNET_TIME_absolute_get (); + ai (ai_cls, + GNUNET_TIME_absolute_subtract (now, + timeframe), + &eval_trigger, + &ttc); + } + if (0 == needed_cnt) + return NULL; + { + struct RemoveContext rc = { + .needed = needed, + .needed_cnt = &needed_cnt + }; + enum GNUNET_DB_QueryStatus qs; + + /* Check what provider checks are already satisfied for h_payto (with + database), remove those from the 'needed' array. */ + GNUNET_break (0); + // FIXME: do via callback! + qs = ki (ki_cls, + h_payto, + & + remove_satisfied, + &rc); + GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? + } + + /* Count maximum number of remaining checks covered by any + provider */ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + unsigned int matched = 0; + + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + for (unsigned int k = 0; k<needed_cnt; k++) + if (kc == needed[k]) + { + matched++; + break; + } + } + max_checks = GNUNET_MAX (max_checks, + matched); + } + + /* Find min-cost provider covering max_checks. */ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + const struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + unsigned int matched = 0; + + for (unsigned int j = 0; j<kp->num_checks; j++) + { + const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; + + for (unsigned int k = 0; k<needed_cnt; k++) + if (kc == needed[k]) + { + matched++; + break; + } + } + if ( (max_checks == matched) && + (kp->cost < min_cost) ) + { + min_cost = kp->cost; + kp_best = kp; + } + } + + GNUNET_assert (NULL != kp_best); + return kp_best->provider_section_name; +} + + +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_kyc_get_logic (const char *provider_section_name, + struct TALER_KYCLOGIC_Plugin **plugin, + struct TALER_KYCLOGIC_ProviderDetails **pd) +{ + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcasecmp (provider_section_name, + kp->provider_section_name)) + continue; + *plugin = kp->logic; + *pd = kp->pd; + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Provider `%s' unknown\n", + provider_section_name); + return GNUNET_SYSERR; +} + + +/* end of taler-exchange-httpd_kyc.c */ diff --git a/src/kyclogic/taler-exchange-kyc-tester.c b/src/kyclogic/taler-exchange-kyc-tester.c index 24809a82d..1fb0afa47 100644 --- a/src/kyclogic/taler-exchange-kyc-tester.c +++ b/src/kyclogic/taler-exchange-kyc-tester.c @@ -28,6 +28,7 @@ #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_crypto_lib.h" +#include "taler_kyclogic_lib.h" #include "taler_kyclogic_plugin.h" #include <gnunet/gnunet_mhd_compat.h> @@ -221,6 +222,328 @@ r404 (struct MHD_Connection *connection, /** + * 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; + + /** + * Logic the request is for. Name of the configuration + * section defining the KYC logic. + */ + char *logic; + + /** + * 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 (); +} + + +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 legi_row legitimization request the webhook was about + * @param account_id account the webhook was about + * @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 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 legi_row, + const struct TALER_PaytoHashP *account_id, + const char *provider_user_id, + const char *provider_legitimization_id, + enum TALER_KYCLOGIC_KycStatus status, + struct GNUNET_TIME_Absolute expiration, + unsigned int http_status, + struct MHD_Response *response) +{ + struct KycWebhookContext *kwh = cls; + + kwh->wh = NULL; + 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); + 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) legi_row, + status); + break; + } + kwh->response = response; + kwh->response_code = http_status; + kwh_resume (kwh); +} + + +/** + * 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->logic); + 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 + * @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) +{ + // FIXME: pass value to use for h_payto via command-line? + memset (h_payto, + 42, + sizeof (*h_payto)); + return 1; // FIXME... +} + + +/** + * 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 payment_target_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->logic = GNUNET_strdup (args[0]); + kwh->rc = rc; + rc->rh_ctx = kwh; + rc->rh_cleaner = &clean_kwh; + + if (GNUNET_OK != + TALER_KYCLOGIC_kyc_get_logic (kwh->logic, + &kwh->plugin, + &kwh->pd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "KYC logic `%s' unknown (check KYC provider configuration)\n", + kwh->logic); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_KYC_WEBHOOK_LOGIC_UNKNOWN, + "$LOGIC"); + } + 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) + { + /* 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_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "resumed without response"); +} + + +static MHD_RESULT +handler_kyc_webhook_get ( + struct TEKT_RequestContext *rc, + const char *const args[]) +{ + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_GET, + NULL, + args); +} + + +static MHD_RESULT +handler_kyc_webhook_post ( + struct TEKT_RequestContext *rc, + const json_t *root, + const char *const args[]) +{ + return handler_kyc_webhook_generic (rc, + MHD_HTTP_METHOD_POST, + root, + args); +} + + +/** * 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 @@ -295,7 +618,6 @@ proceed_with_handler (struct TEKT_RequestContext *rc, size_t *upload_data_size) { const struct TEKT_RequestHandler *rh = rc->rh; - // FIXME: handle '-1 == rh->nargs'!!! const char *args[rh->nargs + 2]; size_t ulen = strlen (url) + 1; json_t *root = NULL; @@ -442,19 +764,21 @@ handle_mhd_request (void *cls, .handler.post = &TEKT_handler_kyc_wallet, .nargs = 0 }, +#endif { .url = "kyc-webhook", .method = MHD_HTTP_METHOD_POST, - .handler.post = &TEKT_handler_kyc_webhook_post, - .nargs = -1 + .handler.post = &handler_kyc_webhook_post, + .nargs = 128, + .nargs_is_upper_bound = true }, { .url = "kyc-webhook", .method = MHD_HTTP_METHOD_GET, - .handler.post = &TEKT_handler_kyc_webhook_get, - .nargs = -1 + .handler.get = &handler_kyc_webhook_get, + .nargs = 128, + .nargs_is_upper_bound = true }, -#endif /* mark end of list */ { .url = NULL @@ -670,6 +994,8 @@ do_shutdown (void *cls) struct MHD_Daemon *mhd; (void) cls; + kyc_webhook_cleanup (); + TALER_KYCLOGIC_kyc_done (); mhd = TALER_MHD_daemon_stop (); if (NULL != mhd) MHD_stop_daemon (mhd); @@ -708,11 +1034,18 @@ run (void *cls, (void ) cfgfile; TALER_MHD_setup (TALER_MHD_GO_NONE); TEKT_cfg = config; - + 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; + TALER_KYCLOGIC_kyc_done (); GNUNET_SCHEDULER_shutdown (); return; } |