diff options
Diffstat (limited to 'src/reducer/anastasis_api_redux.c')
-rw-r--r-- | src/reducer/anastasis_api_redux.c | 1730 |
1 files changed, 1730 insertions, 0 deletions
diff --git a/src/reducer/anastasis_api_redux.c b/src/reducer/anastasis_api_redux.c new file mode 100644 index 0000000..eb7e362 --- /dev/null +++ b/src/reducer/anastasis_api_redux.c @@ -0,0 +1,1730 @@ +/* + This file is part of Anastasis + Copyright (C) 2020, 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 + Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file lib/anastasis_api_redux.c + * @brief anastasis reducer api + * @author Christian Grothoff + * @author Dominik Meister + * @author Dennis Neufeld + */ +#include <platform.h> +#include <jansson.h> +#include "anastasis_redux.h" +#include "anastasis_error_codes.h" +#include <taler/taler_json_lib.h> +#include "anastasis_api_redux.h" +#include <dlfcn.h> + + +/** + * How long do we wait at most for a /config reply from an Anastasis provider. + * 60s is very generous, given the tiny bandwidth required, even for the most + * remote locations. + */ +#define CONFIG_GENERIC_TIMEOUT GNUNET_TIME_UNIT_MINUTES + + +#define GENERATE_STRING(STRING) #STRING, +static const char *generic_strings[] = { + ANASTASIS_GENERIC_STATES (GENERATE_STRING) +}; +#undef GENERATE_STRING + + +/** + * #ANASTASIS_REDUX_add_provider_to_state_ waiting for the + * configuration request to complete or fail. + */ +struct ConfigReduxWaiting +{ + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *prev; + + /** + * Kept in a DLL. + */ + struct ConfigReduxWaiting *next; + + /** + * Associated redux action. + */ + struct ANASTASIS_ReduxAction ra; + + /** + * Config request we are waiting for. + */ + struct ConfigRequest *cr; + + /** + * State we are processing. + */ + json_t *state; + + /** + * Function to call with updated @e state. + */ + ANASTASIS_ActionCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + +}; + + +/** + * Anastasis authorization method configuration + */ +struct AuthorizationMethodConfig +{ + /** + * Type of the method, i.e. "question". + */ + char *type; + + /** + * Fee charged for accessing key share using this method. + */ + struct TALER_Amount usage_fee; +}; + + +/** + * State for a "get config" operation. + */ +struct ConfigRequest +{ + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *next; + + /** + * Kept in a DLL, given that we may have multiple backends. + */ + struct ConfigRequest *prev; + + /** + * Head of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_head; + + /** + * Tail of DLL of REDUX operations waiting for an answer. + */ + struct ConfigReduxWaiting *w_tail; + + /** + * Obtained status code. + */ + unsigned int http_status; + + /** + * The /config GET operation handle. + */ + struct ANASTASIS_ConfigOperation *co; + + /** + * URL of the anastasis backend. + */ + char *url; + + /** + * Business name of the anastasis backend. + */ + char *business_name; + + /** + * currency used by the anastasis backend. + */ + char *currency; + + /** + * Array of authorization methods supported by the server. + */ + struct AuthorizationMethodConfig *methods; + + /** + * Length of the @e methods array. + */ + unsigned int methods_length; + + /** + * Maximum size of an upload in megabytes. + */ + uint32_t storage_limit_in_megabytes; + + /** + * Annual fee for an account / policy upload. + */ + struct TALER_Amount annual_fee; + + /** + * Fee for a truth upload. + */ + struct TALER_Amount truth_upload_fee; + + /** + * Maximum legal liability for data loss covered by the + * provider. + */ + struct TALER_Amount liability_limit; + + /** + * Server salt. + */ + struct ANASTASIS_CRYPTO_ProviderSaltP salt; + + /** + * Task to timeout /config requests. + */ + struct GNUNET_SCHEDULER_Task *tt; + + /** + * Status of the /config request. + */ + enum TALER_ErrorCode ec; +}; + + +/** + * Reducer API's CURL context handle. + */ +struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_; + +/** + * JSON containing country specific identity attributes to ask the user for. + */ +static json_t *redux_id_attr; + +/** + * Head of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_head; + +/** + * Tail of DLL of Anastasis backend configuration requests. + */ +static struct ConfigRequest *cr_tail; + +/** + * JSON containing country specific information. + */ +static json_t *redux_countries; + +/** + * List of Anastasis providers. + */ +static json_t *provider_list; + + +/** + * Extract the mode of a state from json + * + * @param state the state to operate on + * @return "backup_state" or "recovery_state" + */ +static const char * +get_state_mode (const json_t *state) +{ + if (json_object_get (state, "backup_state")) + return "backup_state"; + if (json_object_get (state, "recovery_state")) + return "recovery_state"; + GNUNET_assert (0); + return NULL; +} + + +enum ANASTASIS_GenericState +ANASTASIS_generic_state_from_string_ (const char *state_string) +{ + for (enum ANASTASIS_GenericState i = 0; + i < sizeof (generic_strings) / sizeof(*generic_strings); + i++) + if (0 == strcmp (state_string, + generic_strings[i])) + return i; + return ANASTASIS_GENERIC_STATE_ERROR; +} + + +const char * +ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs) +{ + if ( (gs < 0) || + (gs >= sizeof (generic_strings) / sizeof(*generic_strings)) ) + { + GNUNET_break_op (0); + return NULL; + } + return generic_strings[gs]; +} + + +void +ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb, + void *cb_cls, + enum TALER_ErrorCode ec, + const char *detail) +{ + json_t *estate; + + estate = json_pack ("{s:s?, s:I, s:s}", + "detail", detail, + "code", (json_int_t) ec, + "hint", TALER_ErrorCode_get_hint (ec)); + cb (cb_cls, + ec, + estate); + json_decref (estate); +} + + +/** + * Transition the @a state to @a gs. + * + * @param[in,out] state to transition + * @param gs state to transition to + */ +static void +redux_transition (json_t *state, + enum ANASTASIS_GenericState gs) +{ + const char *s_mode = get_state_mode (state); + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_generic_state_to_string_ (gs)))); + +} + + +void +ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx) +{ + ANASTASIS_REDUX_ctx_ = ctx; +} + + +/** + * Function to free a #ConfigRequest, an async operation. + * + * @param cr state for a "get config" operation + */ +static void +free_config_request (struct ConfigRequest *cr) +{ + GNUNET_assert (NULL == cr->w_head); + if (NULL != cr->co) + ANASTASIS_config_cancel (cr->co); + if (NULL != cr->tt) + GNUNET_SCHEDULER_cancel (cr->tt); + GNUNET_free (cr->currency); + GNUNET_free (cr->url); + GNUNET_free (cr->business_name); + for (unsigned int i = 0; i<cr->methods_length; i++) + GNUNET_free (cr->methods[i].type); + GNUNET_free (cr->methods); + GNUNET_free (cr); +} + + +void +ANASTASIS_redux_done () +{ + struct ConfigRequest *cr; + + while (NULL != (cr = cr_head)) + { + GNUNET_CONTAINER_DLL_remove (cr_head, + cr_tail, + cr); + free_config_request (cr); + } + ANASTASIS_REDUX_ctx_ = NULL; + if (NULL != redux_countries) + { + json_decref (redux_countries); + redux_countries = NULL; + } + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + if (NULL != provider_list) + { + json_decref (provider_list); + provider_list = NULL; + } +} + + +const json_t * +ANASTASIS_redux_countries_init_ (void) +{ + char *dn; + json_error_t error; + + if (NULL != redux_countries) + return redux_countries; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.countries.json", + path); + GNUNET_free (path); + } + redux_countries = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_countries) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + return redux_countries; +} + + +/** + * Abort waiting for /config reply. + * + * @param cls a `struct ConfigReduxWaiting` handle. + */ +static void +abort_provider_config_cb (void *cls) +{ + struct ConfigReduxWaiting *w = cls; + struct ConfigRequest *cr = w->cr; + + GNUNET_CONTAINER_DLL_remove (cr->w_head, + cr->w_tail, + w); + json_decref (w->state); + GNUNET_free (w); +} + + +/** + * Notify anyone waiting on @a cr that the request is done + * (successful or failed). + * + * @param[in,out] cr request that completed + */ +static void +notify_waiting (struct ConfigRequest *cr) +{ + struct ConfigReduxWaiting *w; + + while (NULL != (w = cr->w_head)) + { + json_t *provider_list; + json_t *prov; + + if (NULL == (provider_list = json_object_get (w->state, + "authentication_providers"))) + { + GNUNET_assert (0 == + json_object_set_new (w->state, + "authentication_providers", + provider_list = json_object ())); + } + provider_list = json_object_get (w->state, + "authentication_providers"); + GNUNET_assert (NULL != provider_list); + + if (TALER_EC_NONE != cr->ec) + { + prov = json_pack ("{s:I, s:I}", + "error_code", + (json_int_t) cr->ec, + "http_status", + (json_int_t) cr->http_status); + } + else + { + json_t *methods_list; + + methods_list = json_array (); + GNUNET_assert (NULL != methods_list); + for (unsigned int i = 0; i<cr->methods_length; i++) + { + struct AuthorizationMethodConfig *method = &cr->methods[i]; + json_t *mj = json_pack ("{s:s, s:o}", + "type", + method->type, + "usage_fee", + TALER_JSON_from_amount (&method->usage_fee)); + + GNUNET_assert (NULL != mj); + GNUNET_assert (0 == + json_array_append_new (methods_list, + mj)); + } + prov = json_pack ("{s:o, s:o, s:o, s:o, s:s," + " s:s, s:I, s:o, s:I}", + "methods", + methods_list, + "annual_fee", + TALER_JSON_from_amount (&cr->annual_fee), + "truth_upload_fee", + TALER_JSON_from_amount (&cr->truth_upload_fee), + "liability_limit", + TALER_JSON_from_amount (&cr->liability_limit), + "currency", + cr->currency, + /* 6 */ + "business_name", + cr->business_name, + "storage_limit_in_megabytes", + (json_int_t) cr->storage_limit_in_megabytes, + "salt", + GNUNET_JSON_from_data_auto (&cr->salt), + "http_status", + (json_int_t) cr->http_status); + } + GNUNET_assert (0 == + json_object_set_new (provider_list, + cr->url, + prov)); + w->cb (w->cb_cls, + cr->ec, + w->state); + abort_provider_config_cb (w); + } + +} + + +/** + * Function called with the results of a #ANASTASIS_get_config(). + * + * @param cls closure + * @param http_status HTTP status of the request + * @param acfg anastasis configuration + */ +static void +config_cb (void *cls, + unsigned int http_status, + const struct ANASTASIS_Config *acfg) +{ + struct ConfigRequest *cr = cls; + + cr->co = NULL; + GNUNET_SCHEDULER_cancel (cr->tt); + cr->tt = NULL; + cr->http_status = http_status; + if (MHD_HTTP_OK != http_status) + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + if ( (MHD_HTTP_OK == http_status) && + (NULL == acfg) ) + { + cr->http_status = MHD_HTTP_NOT_FOUND; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED; + } + else if (NULL != acfg) + { + if (0 == acfg->storage_limit_in_megabytes) + { + cr->http_status = 0; + cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG; + } + else + { + cr->currency = GNUNET_strdup (acfg->currency); + cr->business_name = GNUNET_strdup (acfg->business_name); + cr->methods = GNUNET_new_array (acfg->methods_length, + struct AuthorizationMethodConfig); + for (unsigned int i = 0; i<acfg->methods_length; i++) + { + cr->methods[i].type = GNUNET_strdup (acfg->methods[i].type); + cr->methods[i].usage_fee = acfg->methods[i].usage_fee; + } + cr->methods_length = acfg->methods_length; + cr->storage_limit_in_megabytes = acfg->storage_limit_in_megabytes; + cr->annual_fee = acfg->annual_fee; + cr->truth_upload_fee = acfg->truth_upload_fee; + cr->liability_limit = acfg->liability_limit; + cr->salt = acfg->salt; + } + } + notify_waiting (cr); +} + + +/** + * Aborts a "get config" after timeout. + * + * @param cls closure for a "get config" request + */ +static void +config_request_timeout (void *cls) +{ + struct ConfigRequest *cr = cls; + + cr->tt = NULL; + ANASTASIS_config_cancel (cr->co); + cr->co = NULL; + cr->http_status = 0; + cr->ec = TALER_EC_GENERIC_TIMEOUT; + notify_waiting (cr); +} + + +/** + * Schedule job to obtain Anastasis provider configuration at @a url. + * + * @param url base URL of Anastasis provider + * @return check config handle + */ +static struct ConfigRequest * +check_config (const char *url) +{ + struct ConfigRequest *cr; + + for (cr = cr_head; NULL != cr; cr = cr->next) + { + if (0 != strcmp (url, + cr->url)) + continue; + if (NULL != cr->co) + return cr; /* already on it */ + break; + } + if (NULL == cr) + { + cr = GNUNET_new (struct ConfigRequest); + cr->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (cr_head, + cr_tail, + cr); + } + cr->co = ANASTASIS_get_config (ANASTASIS_REDUX_ctx_, + cr->url, + &config_cb, + cr); + if (NULL == cr->co) + { + GNUNET_break (0); + return NULL; + } + else + { + cr->tt = GNUNET_SCHEDULER_add_delayed (CONFIG_GENERIC_TIMEOUT, + &config_request_timeout, + cr); + } + return cr; +} + + +/** + * Begin asynchronous check for provider configurations. + * + * @param currencies the currencies to initiate the provider checks for + * @param[in,out] state to set provider list for + * @return #TALER_EC_NONE on success + */ +static enum TALER_ErrorCode +begin_provider_config_check (const json_t *currencies, + json_t *state) +{ + if (NULL == provider_list) + { + json_error_t error; + char *dn; + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + } + GNUNET_asprintf (&dn, + "%s/provider-list.json", + path); + GNUNET_free (path); + provider_list = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == provider_list) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + GNUNET_free (dn); + } + + { + size_t index; + json_t *provider; + const json_t *provider_arr = json_object_get (provider_list, + "anastasis_provider"); + json_t *pl; + + pl = json_object (); + json_array_foreach (provider_arr, index, provider) + { + const char *url; + const char *cur; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("url", + &url), + GNUNET_JSON_spec_string ("currency", + &cur), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (provider, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED; + } + + { + bool found = false; + json_t *cu; + size_t off; + + json_array_foreach (currencies, off, cu) + { + const char *currency; + + currency = json_string_value (cu); + if (NULL == currency) + { + json_decref (pl); + return TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID; + } + found = (0 == strcasecmp (currency, + cur)); + } + if (! found) + continue; + } + GNUNET_assert (0 == + json_object_set_new (pl, + url, + json_object ())); + check_config (url); + } + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + pl)); + } + return TALER_EC_NONE; +} + + +/** + * Function to validate an input by regular expression ("validation-regex"). + * + * @param input text to validate + * @param regexp regular expression to validate + * @return true if validation passed, else false + */ +static bool +validate_regex (const char *input, + const char *regexp) +{ + regex_t regex; + + if (0 != regcomp (®ex, + regexp, + REG_EXTENDED)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to compile regular expression `%s'.", + regexp); + return true; + } + /* check if input has correct form */ + if (0 != regexec (®ex, + input, + 0, + NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input `%s' does not match regex `%s'\n", + input, + regexp); + regfree (®ex); + return false; + } + regfree (®ex); + return true; +} + + +/** + * Function to load json containing country specific identity + * attributes. Uses a single-slot cache to avoid loading + * exactly the same attributes twice. + * + * @param country_code country code (e.g. "de") + * @return NULL on error + */ +static const json_t * +redux_id_attr_init (const char *country_code) +{ + static char redux_id_cc[3]; + char *dn; + json_error_t error; + + if (0 == strcmp (country_code, + redux_id_cc)) + return redux_id_attr; + + if (NULL != redux_id_attr) + { + json_decref (redux_id_attr); + redux_id_attr = NULL; + } + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return NULL; + } + GNUNET_asprintf (&dn, + "%s/redux.%s.json", + path, + country_code); + GNUNET_free (path); + } + redux_id_attr = json_load_file (dn, + JSON_COMPACT, + &error); + if (NULL == redux_id_attr) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse `%s': %s at %d:%d (%d)\n", + dn, + error.text, + error.line, + error.column, + error.position); + GNUNET_free (dn); + return NULL; + } + GNUNET_free (dn); + strncpy (redux_id_cc, + country_code, + sizeof (redux_id_cc)); + redux_id_cc[2] = '\0'; + return redux_id_attr; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_continent" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +select_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + const json_t *root = json_object_get (redux_countries, + "countries"); + const json_t *continent; + json_t *countries; + + if (NULL == root) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'countries' missing"); + return NULL; + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + continent = json_object_get (arguments, + "continent"); + if (NULL == continent) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' missing"); + return NULL; + } + countries = json_array (); + GNUNET_assert (NULL != countries); + { + size_t index; + const json_t *country; + bool found = false; + + json_array_foreach (root, index, country) + { + json_t *temp_continent = json_object_get (country, + "continent"); + if (1 == json_equal (continent, + temp_continent)) + { + GNUNET_assert (0 == + json_array_append (countries, + (json_t *) country)); + found = true; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'continent' unknown"); + return NULL; + } + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_continent", + (json_t *) continent)); + GNUNET_assert (0 == + json_object_set_new (state, + "countries", + countries)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "select_country" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return #ANASTASIS_ReduxAction + */ +static struct ANASTASIS_ReduxAction * +select_country (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *required_attrs; + const json_t *country_code; + const json_t *currencies; + const json_t *redux_id_attr; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + country_code = json_object_get (arguments, + "country_code"); + if (NULL == country_code) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'country_code' missing"); + return NULL; + } + + { + json_t *countries = json_object_get (state, + "countries"); + size_t index; + json_t *country; + bool found = false; + + json_array_foreach (countries, index, country) + { + json_t *cc = json_object_get (country, + "code"); + if (1 == json_equal (country_code, + cc)) + { + found = true; + break; + } + } + if (! found) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "specified country not on selected continent"); + return NULL; + } + } + + currencies = json_object_get (arguments, + "currencies"); + if ( (NULL == currencies) || + (! json_is_array (currencies)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'currencies' missing"); + return NULL; + } + /* We now have an idea of the currency, begin fetching + provider /configs (we likely need them later) */ + { + enum TALER_ErrorCode ec; + + ec = begin_provider_config_check (currencies, + state); + if (TALER_EC_NONE != ec) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + ec, + NULL); + return NULL; + } + } + redux_id_attr = redux_id_attr_init (json_string_value (country_code)); + if (NULL == redux_id_attr) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MISSING, + json_string_value (country_code)); + return NULL; + } + required_attrs = json_object_get (redux_id_attr, + "required_attributes"); + if (NULL == required_attrs) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED, + "'required_attributes' missing"); + return NULL; + } + redux_transition (state, + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING); + GNUNET_assert (0 == + json_object_set (state, + "selected_country", + (json_t *) country_code)); + GNUNET_assert (0 == + json_object_set (state, + "currencies", + (json_t *) currencies)); + GNUNET_assert (0 == + json_object_set (state, + "required_attributes", + (json_t *) required_attrs)); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "unselect_continent" action. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +unselect_continent (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + redux_transition (state, + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING); + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_REDUX_add_provider_to_state_ (const char *url, + json_t *state, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct ConfigRequest *cr; + struct ConfigReduxWaiting *w; + + cr = check_config (url); + w = GNUNET_new (struct ConfigReduxWaiting); + w->cr = cr; + w->state = json_incref (state); + w->cb = cb; + w->cb_cls = cb_cls; + w->ra.cleanup = &abort_provider_config_cb; + w->ra.cleanup_cls = w; + GNUNET_CONTAINER_DLL_insert (cr->w_head, + cr->w_tail, + w); + if (NULL == cr->co) + { + notify_waiting (cr); + return NULL; + } + return &w->ra; +} + + +/** + * DispatchHandler/Callback function which is called for a + * "enter_user_attributes" action. + * Returns an #ANASTASIS_ReduxAction if operation is async. + * + * @param state state to operate on + * @param arguments arguments to use for operation on state + * @param cb callback to call during/after operation + * @param cb_cls callback closure + * @return NULL + */ +static struct ANASTASIS_ReduxAction * +enter_user_attributes (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const json_t *attributes; + const json_t *required_attributes; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + attributes = json_object_get (arguments, + "identity_attributes"); + if (NULL == attributes) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'identity_attributes' missing"); + return NULL; + } + GNUNET_assert (0 == + json_object_set (state, + "identity_attributes", + (json_t *) attributes)); + + /* Verify required attributes are present and well-formed */ + required_attributes = json_object_get (state, + "required_attributes"); + if ( (NULL == required_attributes) || + (! json_is_array (required_attributes)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' must be an array"); + return NULL; + } + { + size_t index; + json_t *required_attribute; + + json_array_foreach (required_attributes, index, required_attribute) + { + const char *name; + const char *attribute_value; + const char *regexp = NULL; + const char *reglog = NULL; + int optional = false; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-regex", + ®exp)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("validation-logic", + ®log)), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_boolean ("optional", + &optional)), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (required_attribute, + spec, + NULL, NULL)) + { + GNUNET_break (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'required_attributes' lacks required fields"); + return NULL; + } + attribute_value = json_string_value (json_object_get (attributes, + name)); + if (NULL == attribute_value) + { + if (optional) + continue; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Request is missing required attribute `%s'\n", + name); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_GENERIC_PARAMETER_MISSING, + name); + return NULL; + } + if ( (NULL != regexp) && + (! validate_regex (attribute_value, + regexp)) ) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_REGEX_FAILED, + name); + return NULL; + } + + if (NULL != reglog) + { + bool (*regfun)(const char *); + + regfun = dlsym (RTLD_DEFAULT, + reglog); + if (NULL == regfun) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Custom validation function `%s' is not available: %s\n", + reglog, + dlerror ()); + } + else if (! regfun (attribute_value)) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED, + name); + return NULL; + } + } + } /* end for all attributes loop */ + } /* end for all attributes scope */ + + /* Transition based on mode */ + { + const char *s_mode = get_state_mode (state); + + if (0 == strcmp (s_mode, + "backup_state")) + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "backup_state", + json_string ( + ANASTASIS_backup_state_to_string_ ( + ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING)))); + return ANASTASIS_REDUX_backup_begin_ (state, + arguments, + cb, + cb_cls); + } + else + { + GNUNET_assert (0 == + json_object_set_new ( + state, + "recovery_state", + json_string ( + ANASTASIS_recovery_state_to_string_ ( + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING)))); + return ANASTASIS_REDUX_recovery_challenge_begin_ (state, + arguments, + cb, + cb_cls); + } + } +} + + +/** + * DispatchHandler/Callback function which is called for a + * "add_provider" action. Adds another Anastasis provider + * to the list of available providers for storing information. + * + * @param state state to operate on + * @param arguments arguments with a provider URL to add + * @param cb callback to call during/after operation + * @param cb_cls callback closure + */ +static struct ANASTASIS_ReduxAction * +add_provider (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + if (ANASTASIS_add_provider_ (state, + arguments, + cb, + cb_cls)) + return NULL; + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +bool +ANASTASIS_add_provider_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + json_t *urls; + json_t *tlist; + + tlist = json_object_get (state, + "authentication_providers"); + if (NULL == tlist) + { + tlist = json_object (); + GNUNET_assert (NULL != tlist); + GNUNET_assert (0 == + json_object_set_new (state, + "authentication_providers", + tlist)); + } + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return true; + } + urls = json_object_get (arguments, + "urls"); + if (NULL == urls) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' missing"); + return true; + } + { + size_t index; + json_t *url; + + json_array_foreach (urls, index, url) + { + const char *url_str = json_string_value (url); + + if (NULL == url_str) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "'urls' must be strings"); + return true; + } + GNUNET_assert (0 == + json_object_set_new (tlist, + url_str, + json_object ())); + } + } + return false; +} + + +struct ANASTASIS_ReduxAction * +ANASTASIS_back_generic_decrement_ (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + const char *s_mode = get_state_mode (state); + const char *state_string = json_string_value (json_object_get (state, + s_mode)); + + (void) arguments; + GNUNET_assert (NULL != state_string); + if (0 == strcmp ("backup_state", + s_mode)) + { + enum ANASTASIS_BackupState state_index; + + state_index = ANASTASIS_backup_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_backup_state_to_string_ (state_index)))); + } + else + { + enum ANASTASIS_RecoveryState state_index; + + state_index = ANASTASIS_recovery_state_from_string_ (state_string); + GNUNET_assert (state_index > 0); + state_index = state_index - 1; + GNUNET_assert (0 == + json_object_set_new ( + state, + s_mode, + json_string ( + ANASTASIS_recovery_state_to_string_ (state_index)))); + } + cb (cb_cls, + TALER_EC_NONE, + state); + return NULL; +} + + +/** + * Callback function which is called by the reducer in dependence of + * given state and action. + * + * @param state the previous state to operate on + * @param arguments the arguments needed by operation to operate on state + * @param cb Callback function which returns the new state + * @param cb_cls closure for @a cb + * @return handle to cancel async actions, NULL if @a cb was already called + */ +typedef struct ANASTASIS_ReduxAction * +(*DispatchHandler)(json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls); + + +struct ANASTASIS_ReduxAction * +ANASTASIS_redux_action (const json_t *state, + const char *action, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct Dispatcher + { + enum ANASTASIS_GenericState redux_state; + const char *redux_action; + DispatchHandler fun; + } dispatchers[] = { + { + ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_country", + &select_country + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "select_continent", + &select_continent + }, + { + ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING, + "unselect_continent", + &unselect_continent + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "enter_user_attributes", + &enter_user_attributes + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "add_provider", + &add_provider + }, + { + ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING, + "back", + &ANASTASIS_back_generic_decrement_ + }, + { ANASTASIS_GENERIC_STATE_ERROR, NULL, NULL } + }; + bool recovery_mode = false; + const char *s = json_string_value (json_object_get (state, + "backup_state")); + enum ANASTASIS_GenericState gs; + + if (NULL == s) + { + s = json_string_value (json_object_get (state, + "recovery_state")); + if (NULL == s) + { + GNUNET_break_op (0); + cb (cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + NULL); + return NULL; + } + recovery_mode = true; + } + gs = ANASTASIS_generic_state_from_string_ (s); + { + json_t *new_state; + struct ANASTASIS_ReduxAction *ret; + + new_state = json_deep_copy (state); + GNUNET_assert (NULL != new_state); + if (gs != ANASTASIS_GENERIC_STATE_ERROR) + { + for (unsigned int i = 0; NULL != dispatchers[i].fun; i++) + { + if ( (gs == dispatchers[i].redux_state) && + (0 == strcmp (action, + dispatchers[i].redux_action)) ) + { + ret = dispatchers[i].fun (new_state, + arguments, + cb, + cb_cls); + json_decref (new_state); + return ret; + } + } + } + if (recovery_mode) + { + ret = ANASTASIS_recovery_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + else + { + ret = ANASTASIS_backup_action_ (new_state, + action, + arguments, + cb, + cb_cls); + } + json_decref (new_state); + return ret; + } +} + + +void +ANASTASIS_redux_action_cancel (struct ANASTASIS_ReduxAction *ra) +{ + ra->cleanup (ra->cleanup_cls); +} + + +json_t * +ANASTASIS_REDUX_load_continents_ () +{ + const json_t *countries; + json_t *continents; + const json_t *redux_countries = ANASTASIS_redux_countries_init_ (); + + if (NULL == redux_countries) + { + GNUNET_break (0); + return NULL; + } + countries = json_object_get (redux_countries, + "countries"); + if (NULL == countries) + { + GNUNET_break (0); + return NULL; + } + continents = json_array (); + GNUNET_assert (NULL != continents); + + { + json_t *country; + size_t index; + + json_array_foreach (countries, index, country) + { + json_t *ex = NULL; + const json_t *continent; + + continent = json_object_get (country, + "continent"); + if ( (NULL == continent) || + (! json_is_string (continent)) ) + { + GNUNET_break (0); + continue; + } + { + size_t inner_index; + json_t *inner_continent; + + json_array_foreach (continents, inner_index, inner_continent) + { + const json_t *name; + + name = json_object_get (inner_continent, + "name"); + if (1 == json_equal (continent, + name)) + { + ex = inner_continent; + break; + } + } + } + if (NULL == ex) + { + ex = json_pack ("{s:O}", + "name", + continent); + GNUNET_assert (0 == + json_array_append_new (continents, + ex)); + } + + { + json_t *i18n_continent; + json_t *name_ex; + + i18n_continent = json_object_get (country, + "continent_i18n"); + name_ex = json_object_get (ex, + "name_i18n"); + if (NULL != i18n_continent) + { + const char *lang; + json_t *trans; + + json_object_foreach (i18n_continent, lang, trans) + { + if (NULL == name_ex) + { + name_ex = json_object (); + json_object_set_new (ex, + "name_i18n", + name_ex); + } + if (NULL == json_object_get (name_ex, + lang)) + { + json_object_set (name_ex, + lang, + trans); + } + } + } + } + } + } + return json_pack ("{s:o}", + "continents", + continents); +} |