diff options
Diffstat (limited to 'src/backend')
-rw-r--r-- | src/backend/Makefile.am | 11 | ||||
-rw-r--r-- | src/backend/anastasis-httpd.c | 152 | ||||
-rw-r--r-- | src/backend/anastasis-httpd.h | 9 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_config.c | 115 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_policy-meta.c | 192 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_policy-meta.h | 41 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_policy-upload.c (renamed from src/backend/anastasis-httpd_policy_upload.c) | 222 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_policy.c | 36 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_policy.h | 2 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_terms.c | 23 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_terms.h | 2 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_truth-challenge.c (renamed from src/backend/anastasis-httpd_truth.c) | 801 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_truth-solve.c | 1474 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_truth-upload.c (renamed from src/backend/anastasis-httpd_truth_upload.c) | 249 | ||||
-rw-r--r-- | src/backend/anastasis-httpd_truth.h | 59 | ||||
-rw-r--r-- | src/backend/anastasis.conf | 6 |
16 files changed, 2420 insertions, 974 deletions
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am index 1046810..c1b0931 100644 --- a/src/backend/Makefile.am +++ b/src/backend/Makefile.am @@ -18,11 +18,14 @@ anastasis_httpd_SOURCES = \ anastasis-httpd.c anastasis-httpd.h \ anastasis-httpd_mhd.c anastasis-httpd_mhd.h \ anastasis-httpd_policy.c anastasis-httpd_policy.h \ - anastasis-httpd_policy_upload.c \ - anastasis-httpd_truth.c anastasis-httpd_truth.h \ + anastasis-httpd_policy-meta.c anastasis-httpd_policy-meta.h \ + anastasis-httpd_policy-upload.c \ + anastasis-httpd_truth.h \ anastasis-httpd_terms.c anastasis-httpd_terms.h \ anastasis-httpd_config.c anastasis-httpd_config.h \ - anastasis-httpd_truth_upload.c + anastasis-httpd_truth-challenge.c \ + anastasis-httpd_truth-solve.c \ + anastasis-httpd_truth-upload.c anastasis_httpd_LDADD = \ $(top_builddir)/src/util/libanastasisutil.la \ @@ -34,11 +37,9 @@ anastasis_httpd_LDADD = \ -ltalerjson \ -ltalerutil \ -lgnunetcurl \ - -lgnunetrest \ -lgnunetjson \ -lgnunetutil \ -lmicrohttpd \ - -luuid \ $(XLIB) EXTRA_DIST = \ diff --git a/src/backend/anastasis-httpd.c b/src/backend/anastasis-httpd.c index 9f5c87b..ebfb0ae 100644 --- a/src/backend/anastasis-httpd.c +++ b/src/backend/anastasis-httpd.c @@ -1,6 +1,6 @@ /* This file is part of Anastasis - (C) 2020 Anastasis SARL + (C) 2020-2022 Anastasis SARL Anastasis 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 @@ -26,17 +26,13 @@ #include "anastasis-httpd_mhd.h" #include "anastasis_database_lib.h" #include "anastasis-httpd_policy.h" +#include "anastasis-httpd_policy-meta.h" #include "anastasis-httpd_truth.h" #include "anastasis-httpd_terms.h" #include "anastasis-httpd_config.h" /** - * Backlog for listen operation on unix-domain sockets. - */ -#define UNIX_BACKLOG 500 - -/** * Upload limit to the service, in megabytes. */ unsigned long long int AH_upload_limit_mb; @@ -72,11 +68,6 @@ const struct GNUNET_CONFIGURATION_Handle *AH_cfg; char *AH_backend_url; /** - * Taler currency. - */ -char *AH_currency; - -/** * Our fulfillment URL. */ char *AH_fulfillment_url; @@ -87,9 +78,9 @@ char *AH_fulfillment_url; char *AH_business_name; /** - * Our server salt. + * Our provider salt. */ -struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; +struct ANASTASIS_CRYPTO_ProviderSaltP AH_provider_salt; /** * Number of policy uploads permitted per annual fee payment. @@ -295,7 +286,7 @@ url_handler (void *cls, &TMH_MHD_handler_agpl_redirect, MHD_HTTP_FOUND }, { "/terms", MHD_HTTP_METHOD_GET, NULL, NULL, 0, - &AH_handler_terms, MHD_HTTP_OK }, + &AH_handler_privacy, MHD_HTTP_OK }, { "/privacy", MHD_HTTP_METHOD_GET, NULL, NULL, 0, &AH_handler_terms, MHD_HTTP_OK }, @@ -360,12 +351,15 @@ url_handler (void *cls, strlen ("/policy/"))) { const char *account = url + strlen ("/policy/"); + const char *end = strchr (account, '/'); struct ANASTASIS_CRYPTO_AccountPublicKeyP account_pub; if (GNUNET_OK != GNUNET_STRINGS_string_to_data ( account, - strlen (account), + (NULL == end) + ? strlen (account) + : end - account, &account_pub, sizeof (struct ANASTASIS_CRYPTO_AccountPublicKeyP))) { @@ -374,14 +368,23 @@ url_handler (void *cls, TALER_EC_GENERIC_PARAMETER_MALFORMED, "account public key"); } + if ( (NULL != end) && + (0 != strcmp (end, + "/meta")) ) + return TMH_MHD_handler_static_response (&h404, + connection); if (0 == strcmp (method, MHD_HTTP_METHOD_GET)) { - return AH_policy_get (connection, - &account_pub); + if (NULL == end) + return AH_policy_get (connection, + &account_pub); + return AH_policy_meta_get (connection, + &account_pub); } - if (0 == strcmp (method, - MHD_HTTP_METHOD_POST)) + if ( (0 == strcmp (method, + MHD_HTTP_METHOD_POST)) && + (NULL == end) ) { return AH_handler_policy_post (connection, hc, @@ -389,6 +392,11 @@ url_handler (void *cls, upload_data, upload_data_size); } + if (0 == strcmp (method, + MHD_HTTP_METHOD_OPTIONS)) + { + return TALER_MHD_reply_cors_preflight (connection); + } return TMH_MHD_handler_static_response (&h405, connection); } @@ -398,12 +406,20 @@ url_handler (void *cls, { struct ANASTASIS_CRYPTO_TruthUUIDP tu; const char *pub_key_str; + const char *end; + size_t len; pub_key_str = &url[strlen ("/truth/")]; + end = strchr (pub_key_str, + '/'); + if (NULL == end) + len = strlen (pub_key_str); + else + len = end - pub_key_str; if (GNUNET_OK != GNUNET_STRINGS_string_to_data ( pub_key_str, - strlen (pub_key_str), + len, &tu, sizeof(tu))) { @@ -413,15 +429,19 @@ url_handler (void *cls, TALER_EC_GENERIC_PARAMETER_MALFORMED, "truth UUID"); } + if ( (NULL != end) && + (0 != strcmp (end, "/solve")) && + (0 != strcmp (end, "/challenge")) ) + return TMH_MHD_handler_static_response (&h404, + connection); if (0 == strcmp (method, - MHD_HTTP_METHOD_GET)) - { - return AH_handler_truth_get (connection, - &tu, - hc); - } - if (0 == strcmp (method, + MHD_HTTP_METHOD_OPTIONS)) + return TALER_MHD_reply_cors_preflight (connection); + if (0 != strcmp (method, MHD_HTTP_METHOD_POST)) + return TMH_MHD_handler_static_response (&h405, + connection); + if (NULL == end) { return AH_handler_truth_post (connection, hc, @@ -429,9 +449,27 @@ url_handler (void *cls, upload_data, upload_data_size); } - return TMH_MHD_handler_static_response (&h405, - connection); - } + if (0 == strcmp (end, + "/solve")) + { + return AH_handler_truth_solve (connection, + hc, + &tu, + upload_data, + upload_data_size); + } + if (0 == strcmp (end, + "/challenge")) + { + return AH_handler_truth_challenge (connection, + hc, + &tu, + upload_data, + upload_data_size); + } + /* should be impossible to get here */ + GNUNET_assert (0); + } /* end of "/truth/" prefix */ path_matched = false; for (unsigned int i = 0; NULL != handlers[i].url; i++) { @@ -474,7 +512,8 @@ do_shutdown (void *cls) { (void) cls; AH_resume_all_bc (); - AH_truth_shutdown (); + AH_truth_challenge_shutdown (); + AH_truth_solve_shutdown (); AH_truth_upload_shutdown (); if (NULL != mhd_task) { @@ -712,34 +751,6 @@ run (void *cls, return; } if (GNUNET_OK != - TALER_config_get_currency (config, - &AH_currency)) - { - GNUNET_SCHEDULER_shutdown (); - return; - } - if (0 != strcasecmp (AH_currency, - AH_annual_fee.currency)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "anastasis", - "ANNUAL_FEE", - "currency mismatch"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != - TALER_amount_cmp_currency (&AH_insurance, - &AH_annual_fee)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, - "anastasis", - "INSURANCE", - "currency mismatch"); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (config, "anastasis-merchant-backend", "PAYMENT_BACKEND_URL", @@ -817,30 +828,30 @@ run (void *cls, return; } { - char *server_salt; + char *provider_salt; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (config, "anastasis", - "SERVER_SALT", - &server_salt)) + "PROVIDER_SALT", + &provider_salt)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "anastasis", - "SERVER_SALT"); + "PROVIDER_SALT"); GNUNET_SCHEDULER_shutdown (); return; } GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (&AH_server_salt, - sizeof (AH_server_salt), - "anastasis-server-salt", - strlen ("anastasis-server-salt"), - server_salt, - strlen (server_salt), + GNUNET_CRYPTO_kdf (&AH_provider_salt, + sizeof (AH_provider_salt), + "anastasis-provider-salt", + strlen ("anastasis-provider-salt"), + provider_salt, + strlen (provider_salt), NULL, 0)); - GNUNET_free (server_salt); + GNUNET_free (provider_salt); } /* setup HTTP client event loop */ @@ -971,7 +982,6 @@ main (int argc, "CERTTYPE", "type of the TLS client certificate, defaults to PEM if not specified", &certtype), - GNUNET_GETOPT_OPTION_END }; diff --git a/src/backend/anastasis-httpd.h b/src/backend/anastasis-httpd.h index 33e0504..1a87921 100644 --- a/src/backend/anastasis-httpd.h +++ b/src/backend/anastasis-httpd.h @@ -175,11 +175,6 @@ extern struct TALER_Amount AH_question_cost; extern char *AH_backend_url; /** - * Taler currency. - */ -extern char *AH_currency; - -/** * Heap for processing timeouts of requests. */ extern struct GNUNET_CONTAINER_Heap *AH_to_heap; @@ -205,9 +200,9 @@ extern char *AH_fulfillment_url; extern char *AH_business_name; /** - * Our server salt. + * Our provider salt. */ -extern struct ANASTASIS_CRYPTO_ProviderSaltP AH_server_salt; +extern struct ANASTASIS_CRYPTO_ProviderSaltP AH_provider_salt; /** * Our context for making HTTP requests. diff --git a/src/backend/anastasis-httpd_config.c b/src/backend/anastasis-httpd_config.c index d96d57e..315419e 100644 --- a/src/backend/anastasis-httpd_config.c +++ b/src/backend/anastasis-httpd_config.c @@ -1,9 +1,9 @@ /* This file is part of Anastasis - Copyright (C) 2020, 2021 Anastasis SARL + Copyright (C) 2020, 2021, 2024 Anastasis SARL 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 + 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. Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY @@ -78,48 +78,83 @@ MHD_RESULT AH_handler_config (struct AH_RequestHandler *rh, struct MHD_Connection *connection) { - json_t *method_arr = json_array (); + static struct MHD_Response *response; + static struct GNUNET_TIME_Absolute a; - GNUNET_assert (NULL != method_arr); + if ( (GNUNET_TIME_absolute_is_past (a)) && + (NULL != response) ) { - json_t *method; + MHD_destroy_response (response); + response = NULL; + } + if (NULL == response) + { + json_t *method_arr = json_array (); + struct GNUNET_TIME_Timestamp km; + char dat[128]; + + GNUNET_assert (NULL != method_arr); + a = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_DAYS); + /* Round up to next full day to ensure the expiration + time does not become a fingerprint! */ + a = GNUNET_TIME_absolute_round_down (a, + GNUNET_TIME_UNIT_DAYS); + a = GNUNET_TIME_absolute_add (a, + GNUNET_TIME_UNIT_DAYS); + /* => /config response stays at most 48h in caches! */ + km = GNUNET_TIME_absolute_to_timestamp (a); + TALER_MHD_get_date_string (km.abs_time, + dat); + { + json_t *method; + + method = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "question"), + TALER_JSON_pack_amount ("cost", + &AH_question_cost)); + GNUNET_assert ( + 0 == + json_array_append_new (method_arr, + method)); + } + GNUNET_CONFIGURATION_iterate_sections (AH_cfg, + &add_methods, + method_arr); - method = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "question"), - TALER_JSON_pack_amount ("cost", - &AH_question_cost)); - GNUNET_assert ( - 0 == - json_array_append_new (method_arr, - method)); + response = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + "anastasis"), + GNUNET_JSON_pack_string ("version", + "0:2:0"), + GNUNET_JSON_pack_string ("implementation", + "urn:net:taler:specs:anastasis:c-reference"), + GNUNET_JSON_pack_string ("business_name", + AH_business_name), + GNUNET_JSON_pack_array_steal ("methods", + method_arr), + GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes", + AH_upload_limit_mb), + TALER_JSON_pack_amount ("annual_fee", + &AH_annual_fee), + TALER_JSON_pack_amount ("truth_upload_fee", + &AH_truth_upload_fee), + TALER_JSON_pack_amount ("liability_limit", + &AH_insurance), + GNUNET_JSON_pack_data_auto ("provider_salt", + &AH_provider_salt)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_EXPIRES, + dat)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "public,max-age=21600")); /* 6h */ } - GNUNET_CONFIGURATION_iterate_sections (AH_cfg, - &add_methods, - method_arr); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("name", - "anastasis"), - GNUNET_JSON_pack_string ("version", - "0:0:0"), - GNUNET_JSON_pack_string ("business_name", - AH_business_name), - GNUNET_JSON_pack_string ("currency", - (char *) AH_currency), - GNUNET_JSON_pack_array_steal ("methods", - method_arr), - GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes", - AH_upload_limit_mb), - TALER_JSON_pack_amount ("annual_fee", - &AH_annual_fee), - TALER_JSON_pack_amount ("truth_upload_fee", - &AH_truth_upload_fee), - TALER_JSON_pack_amount ("liability_limit", - &AH_insurance), - GNUNET_JSON_pack_data_auto ("server_salt", - &AH_server_salt)); + return MHD_queue_response (connection, + MHD_HTTP_OK, + response); } diff --git a/src/backend/anastasis-httpd_policy-meta.c b/src/backend/anastasis-httpd_policy-meta.c new file mode 100644 index 0000000..67acc52 --- /dev/null +++ b/src/backend/anastasis-httpd_policy-meta.c @@ -0,0 +1,192 @@ +/* + This file is part of Anastasis + Copyright (C) 2022 Anastasis SARL + + Anastasis 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. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_policy-meta.c + * @brief functions to handle incoming requests on /policy/$PID/meta + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis-httpd_policy-meta.h" +#include "anastasis_service.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#include <taler/taler_signatures.h> + + +/** + * Function called on matching meta data. Note that if the client did + * not provide meta data for @a version, the function will be called + * with @a recovery_meta_data being NULL. + * + * @param cls closure with a `json_t *` to build up + * @param version the version of the recovery document + * @param ts timestamp when the document was created + * @param recovery_meta_data contains meta data about the encrypted recovery document + * @param recovery_meta_data_size size of @a recovery_meta_data blob + * @return #GNUNET_OK to continue to iterate, #GNUNET_NO to abort iteration + */ +static enum GNUNET_GenericReturnValue +build_meta_result (void *cls, + uint32_t version, + struct GNUNET_TIME_Timestamp ts, + const void *recovery_meta_data, + size_t recovery_meta_data_size) +{ + json_t *result = cls; + char version_s[14]; + + GNUNET_snprintf (version_s, + sizeof (version_s), + "%u", + (unsigned int) version); + GNUNET_assert (0 == + json_object_set_new ( + result, + version_s, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_varsize ( + "meta", + recovery_meta_data, + recovery_meta_data_size), + GNUNET_JSON_pack_timestamp ( + "upload_time", + ts)))); + return GNUNET_OK; +} + + +/** + * Return the meta data on recovery documents of @a account on @a + * connection. + * + * @param connection MHD connection to use + * @param account_pub account to query + * @return MHD result code + */ +static MHD_RESULT +return_policy_meta ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + enum GNUNET_DB_QueryStatus qs; + uint32_t max_version = INT32_MAX; /* Postgres is using signed ints... */ + json_t *result; + + { + const char *version_s; + + version_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "max_version"); + if (NULL != version_s) + { + char dummy; + + if (1 != sscanf (version_s, + "%u%c", + &max_version, + &dummy)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "version"); + } + if (max_version > INT32_MAX) + max_version = INT32_MAX; /* cap to signed range */ + } + } + result = json_object (); + GNUNET_assert (NULL != result); + qs = db->get_recovery_meta_data (db->cls, + account_pub, + max_version, + &build_meta_result, + result); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_recovery_document"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_recovery_document"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_POLICY_NOT_FOUND, + NULL); + default: + /* interesting case below */ + break; + } + + return TALER_MHD_reply_json_steal (connection, + result, + MHD_HTTP_OK); +} + + +MHD_RESULT +AH_policy_meta_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub) +{ + struct GNUNET_HashCode recovery_data_hash; + enum ANASTASIS_DB_AccountStatus as; + uint32_t version; + struct GNUNET_TIME_Timestamp expiration; + + as = db->lookup_account (db->cls, + account_pub, + &expiration, + &recovery_data_hash, + &version); + switch (as) + { + case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_POLICY_NOT_FOUND, + "account expired: payment required"); + case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup account"); + case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_POLICY_NOT_FOUND, + "no such account"); + case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED: + /* We have results, should fetch and return them! */ + break; + } + return return_policy_meta (connection, + account_pub); +} diff --git a/src/backend/anastasis-httpd_policy-meta.h b/src/backend/anastasis-httpd_policy-meta.h new file mode 100644 index 0000000..8c48fc6 --- /dev/null +++ b/src/backend/anastasis-httpd_policy-meta.h @@ -0,0 +1,41 @@ +/* + This file is part of Anastasis + Copyright (C) 2022 Anastasis SARL + + Anastasis 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. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_policy-meta.h + * @brief functions to handle incoming requests on /policy/ + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#ifndef ANASTASIS_HTTPD_POLICY_META_H +#define ANASTASIS_HTTPD_POLICY_META_H +#include <microhttpd.h> + + +/** + * Handle GET /policy/$ACCOUNT_PUB/meta request. + * + * @param connection the MHD connection to handle + * @param account_pub public key of the account + * @return MHD result code + */ +MHD_RESULT +AH_policy_meta_get ( + struct MHD_Connection *connection, + const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub); + + +#endif diff --git a/src/backend/anastasis-httpd_policy_upload.c b/src/backend/anastasis-httpd_policy-upload.c index c36cc17..83e8117 100644 --- a/src/backend/anastasis-httpd_policy_upload.c +++ b/src/backend/anastasis-httpd_policy-upload.c @@ -35,7 +35,7 @@ * we are awaiting payment before giving up? */ #define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 30) + GNUNET_TIME_UNIT_SECONDS, 30) /** @@ -86,6 +86,16 @@ struct PolicyUploadContext char *upload; /** + * Meta data uploaded by the client, or NULL for none. + */ + void *meta_data; + + /** + * Number of bytes in @e meta_data. + */ + size_t meta_data_size; + + /** * Used while we are awaiting proposal creation. */ struct TALER_MERCHANT_PostOrdersHandle *po; @@ -114,7 +124,7 @@ struct PolicyUploadContext * Timestamp of the order in @e payment_identifier. Used to * select the most recent unpaid offer. */ - struct GNUNET_TIME_Absolute existing_pi_timestamp; + struct GNUNET_TIME_Timestamp existing_pi_timestamp; /** * When does the operation timeout? @@ -125,13 +135,13 @@ struct PolicyUploadContext * How long must the account be valid? Determines whether we should * trigger payment, and if so how much. */ - struct GNUNET_TIME_Absolute end_date; + struct GNUNET_TIME_Timestamp end_date; /** * How long is the account already valid? * Determines how much the user needs to pay. */ - struct GNUNET_TIME_Absolute paid_until; + struct GNUNET_TIME_Timestamp paid_until; /** * Expected total upload size. @@ -220,6 +230,7 @@ cleanup_ctx (struct TM_HandlerContext *hc) if (NULL != puc->resp) MHD_destroy_response (puc->resp); GNUNET_free (puc->upload); + GNUNET_free (puc->meta_data); GNUNET_free (puc); } @@ -323,11 +334,10 @@ proposal_cb (void *cls, AH_trigger_daemon (NULL); if (MHD_HTTP_OK != por->hr.http_status) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Backend returned status %u/%d\n", + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Backend returned status %u/%d when trying to setup order\n", por->hr.http_status, (int) por->hr.ec); - GNUNET_break (0); puc->resp = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_uint64 ("code", TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR), @@ -340,7 +350,7 @@ proposal_cb (void *cls, GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("backend-reply", (json_t *) por->hr.reply))); - puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + puc->response_code = MHD_HTTP_BAD_GATEWAY; return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -377,15 +387,14 @@ proposal_cb (void *cls, * Callback to process a GET /check-payment request * * @param cls our `struct PolicyUploadContext` - * @param hr HTTP response details * @param osr order status */ static void check_payment_cb (void *cls, - const struct TALER_MERCHANT_HttpResponse *hr, const struct TALER_MERCHANT_OrderStatusResponse *osr) { struct PolicyUploadContext *puc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; /* refunds are not supported, verify */ puc->cpo = NULL; @@ -412,10 +421,12 @@ check_payment_cb (void *cls, puc->response_code = MHD_HTTP_BAD_GATEWAY; return; } + + GNUNET_assert (MHD_HTTP_OK == hr->http_status); GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Payment status checked: %s\n", - osr->status ? "paid" : "unpaid"); - switch (osr->status) + "Payment status checked: %d\n", + osr->details.ok.status); + switch (osr->details.ok.status) { case TALER_MERCHANT_OSC_PAID: { @@ -425,13 +436,12 @@ check_payment_cb (void *cls, const json_t *contract; struct TALER_Amount amount; struct GNUNET_JSON_Specification cspec[] = { - TALER_JSON_spec_amount ("amount", - AH_currency, - &amount), + TALER_JSON_spec_amount_any ("amount", + &amount), GNUNET_JSON_spec_end () }; - contract = osr->details.paid.contract_terms; + contract = osr->details.ok.details.paid.contract_terms; if (GNUNET_OK != GNUNET_JSON_parse (contract, cspec, @@ -476,7 +486,7 @@ check_payment_cb (void *cls, case TALER_MERCHANT_OSC_CLAIMED: break; } - if (0 != puc->existing_pi_timestamp.abs_value_us) + if (! GNUNET_TIME_absolute_is_zero (puc->existing_pi_timestamp.abs_time)) { /* repeat payment request */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -527,7 +537,6 @@ await_payment (struct PolicyUploadContext *puc) AH_backend_url, order_id, NULL /* our payments are NOT session-bound */, - false, timeout, &check_payment_cb, puc); @@ -548,6 +557,7 @@ await_payment (struct PolicyUploadContext *puc) static MHD_RESULT begin_payment (struct PolicyUploadContext *puc) { + static const char *no_uuids[1] = { NULL }; json_t *order; GNUNET_CONTAINER_DLL_insert (puc_head, @@ -575,6 +585,10 @@ begin_payment (struct PolicyUploadContext *puc) order_id = GNUNET_STRINGS_data_to_string_alloc ( &puc->payment_identifier, sizeof(struct ANASTASIS_PaymentSecretP)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating order for %u years with payment of %s\n", + puc->years_to_pay, + TALER_amount2s (&upload_fee)); order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s }", "amount", TALER_JSON_from_amount (&upload_fee), "summary", "Anastasis policy storage fee", @@ -594,7 +608,7 @@ begin_payment (struct PolicyUploadContext *puc) 0, NULL, /* no inventory products */ 0, - NULL, /* no uuids */ + no_uuids, /* no uuids */ false, /* do NOT require claim token */ &proposal_cb, puc); @@ -648,35 +662,44 @@ AH_handler_policy_post ( hc->cc = &cleanup_ctx; puc->con = connection; + TALER_MHD_parse_request_header_auto (connection, + ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER, + &puc->payment_identifier, + puc->payment_identifier_provided); + puc->account = *account_pub; + + /* check for meta-data */ { - const char *pay_id; + const char *metas; - pay_id = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); - if (NULL != pay_id) + metas = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_POLICY_META_DATA); + if (NULL == metas) { - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - pay_id, - strlen (pay_id), - &puc->payment_identifier, - sizeof (struct ANASTASIS_PaymentSecretP))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER - " header must be a base32-encoded Payment-Secret"); - } - puc->payment_identifier_provided = true; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Policy upload started with payment identifier `%s'\n", - pay_id); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + ANASTASIS_HTTP_HEADER_POLICY_META_DATA + " header must be present"); + } + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data_alloc (metas, + strlen (metas), + &puc->meta_data, + &puc->meta_data_size)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + ANASTASIS_HTTP_HEADER_POLICY_META_DATA + " header must include a base32-encoded value"); } } - puc->account = *account_pub; /* now setup 'puc' */ { const char *lens; @@ -721,28 +744,10 @@ AH_handler_policy_post ( } puc->upload_size = (size_t) len; } - { - /* Check if header contains Anastasis-Policy-Signature */ - const char *sig_s; - sig_s = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE); - if ( (NULL == sig_s) || - (GNUNET_OK != - GNUNET_STRINGS_string_to_data (sig_s, - strlen (sig_s), - &puc->account_sig, - sizeof (puc->account_sig))) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE, - ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE - " header must include a base32-encoded EdDSA signature"); - } - } + TALER_MHD_parse_request_header_auto_t (connection, + ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE, + &puc->account_sig); { /* Check if header contains an ETAG */ const char *etag; @@ -751,9 +756,12 @@ AH_handler_policy_post ( MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_NONE_MATCH); if ( (NULL == etag) || + (2 >= strlen (etag)) || + ('"' != etag[0]) || + ('"' != etag[strlen (etag) - 1]) || (GNUNET_OK != - GNUNET_STRINGS_string_to_data (etag, - strlen (etag), + GNUNET_STRINGS_string_to_data (etag + 1, + strlen (etag) - 2, &puc->new_policy_upload_hash, sizeof (puc->new_policy_upload_hash))) ) { @@ -787,39 +795,10 @@ AH_handler_policy_post ( } } - { - const char *long_poll_timeout_ms; - - long_poll_timeout_ms = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != long_poll_timeout_ms) - { - unsigned int timeout; - char dummy; - - if (1 != sscanf (long_poll_timeout_ms, - "%u%c", - &timeout, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms (must be non-negative number)"); - } - puc->timeout - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_MILLISECONDS, - timeout)); - } - else - { - puc->timeout = GNUNET_TIME_relative_to_absolute - (CHECK_PAYMENT_GENERIC_TIMEOUT); - } - } + puc->timeout = GNUNET_TIME_relative_to_absolute ( + CHECK_PAYMENT_GENERIC_TIMEOUT); + TALER_MHD_parse_request_timeout (connection, + &puc->timeout); /* check if the client insists on paying */ { @@ -847,9 +826,9 @@ AH_handler_policy_post ( } else { - years = 0; + years = 1; } - puc->end_date = GNUNET_TIME_relative_to_absolute ( + puc->end_date = GNUNET_TIME_relative_to_timestamp ( GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, years)); } @@ -861,11 +840,15 @@ AH_handler_policy_post ( { struct GNUNET_TIME_Relative rem; - rem = GNUNET_TIME_absolute_get_remaining (puc->end_date); + rem = GNUNET_TIME_absolute_get_remaining (puc->end_date.abs_time); puc->years_to_pay = rem.rel_value_us / GNUNET_TIME_UNIT_YEARS.rel_value_us; if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) puc->years_to_pay++; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calculated years to pay to be %u until %s\n", + puc->years_to_pay, + GNUNET_TIME_absolute2s (puc->end_date.abs_time)); if (puc->payment_identifier_provided) { @@ -906,18 +889,14 @@ AH_handler_policy_post ( if (! puc->payment_identifier_provided) { - struct TALER_Amount zero_amount; enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Relative rel; - TALER_amount_set_zero (AH_currency, - &zero_amount); /* generate fresh payment identifier */ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, &puc->payment_identifier, sizeof (struct ANASTASIS_PaymentSecretP)); - if (0 != TALER_amount_cmp (&AH_annual_fee, - &zero_amount)) + if (! TALER_amount_is_zero (&AH_annual_fee)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "No payment identifier, requesting payment\n"); @@ -941,10 +920,10 @@ AH_handler_policy_post ( ANASTASIS_MAX_YEARS_STORAGE); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Policy lifetime is %s (%u years)\n", - GNUNET_STRINGS_relative_time_to_string (rel, - GNUNET_YES), + GNUNET_TIME_relative2s (rel, + true), ANASTASIS_MAX_YEARS_STORAGE); - puc->paid_until = GNUNET_TIME_relative_to_absolute (rel); + puc->paid_until = GNUNET_TIME_relative_to_timestamp (rel); qs = db->update_lifetime (db->cls, account_pub, &puc->payment_identifier, @@ -965,7 +944,7 @@ AH_handler_policy_post ( struct GNUNET_HashCode hc; enum ANASTASIS_DB_AccountStatus as; uint32_t version; - struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Timestamp now; struct GNUNET_TIME_Relative rem; as = db->lookup_account (db->cls, @@ -973,16 +952,21 @@ AH_handler_policy_post ( &puc->paid_until, &hc, &version); - now = GNUNET_TIME_absolute_get (); - if (puc->paid_until.abs_value_us < now.abs_value_us) + now = GNUNET_TIME_timestamp_get (); + if (GNUNET_TIME_timestamp_cmp (puc->paid_until, + <, + now)) puc->paid_until = now; - rem = GNUNET_TIME_absolute_get_difference (puc->paid_until, - puc->end_date); + rem = GNUNET_TIME_absolute_get_difference (puc->paid_until.abs_time, + puc->end_date.abs_time); puc->years_to_pay = rem.rel_value_us / GNUNET_TIME_UNIT_YEARS.rel_value_us; if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) puc->years_to_pay++; - + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calculated years to pay to be %u until %s\n", + puc->years_to_pay, + GNUNET_TIME_absolute2s (puc->end_date.abs_time)); if ( (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED == as) && (0 != puc->years_to_pay) ) { @@ -1113,6 +1097,8 @@ AH_handler_policy_post ( &puc->new_policy_upload_hash, puc->upload, puc->upload_size, + puc->meta_data, + puc->meta_data_size, &puc->payment_identifier, &version); GNUNET_snprintf (version_s, @@ -1123,7 +1109,7 @@ AH_handler_policy_post ( sizeof (expir_s), "%llu", (unsigned long long) - (puc->paid_until.abs_value_us + (puc->paid_until.abs_time.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us)); switch (ss) { diff --git a/src/backend/anastasis-httpd_policy.c b/src/backend/anastasis-httpd_policy.c index 165be44..177cc06 100644 --- a/src/backend/anastasis-httpd_policy.c +++ b/src/backend/anastasis-httpd_policy.c @@ -30,17 +30,9 @@ #include <taler/taler_merchant_service.h> #include <taler/taler_signatures.h> -/** - * How long do we hold an HTTP client connection if - * we are awaiting payment before giving up? - */ -#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 30) - /** - * Return the current recoverydocument of @a account on @a connection - * using @a default_http_status on success. + * Return the current recoverydocument of @a account on @a connection. * * @param connection MHD connection to use * @param account_pub account to query @@ -130,25 +122,30 @@ return_policy (struct MHD_Connection *connection, { char *sig_s; char *etag; + char *etagq; sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig, sizeof (account_sig)); - etag = GNUNET_STRINGS_data_to_string_alloc (&recovery_data_hash, - sizeof (recovery_data_hash)); GNUNET_break (MHD_YES == MHD_add_response_header (resp, ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE, sig_s)); + GNUNET_free (sig_s); GNUNET_break (MHD_YES == MHD_add_response_header (resp, ANASTASIS_HTTP_HEADER_POLICY_VERSION, version_s)); + etag = GNUNET_STRINGS_data_to_string_alloc (&recovery_data_hash, + sizeof (recovery_data_hash)); + GNUNET_asprintf (&etagq, + "\"%s\"", + etag); + GNUNET_free (etag); GNUNET_break (MHD_YES == MHD_add_response_header (resp, MHD_HTTP_HEADER_ETAG, - etag)); - GNUNET_free (etag); - GNUNET_free (sig_s); + etagq)); + GNUNET_free (etagq); } { MHD_RESULT ret; @@ -170,7 +167,7 @@ AH_policy_get (struct MHD_Connection *connection, enum ANASTASIS_DB_AccountStatus as; MHD_RESULT ret; uint32_t version; - struct GNUNET_TIME_Absolute expiration; + struct GNUNET_TIME_Timestamp expiration; as = db->lookup_account (db->cls, account_pub, @@ -211,13 +208,16 @@ AH_policy_get (struct MHD_Connection *connection, inm = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_NONE_MATCH); - if (NULL != inm) + if ( (NULL != inm) && + (2 < strlen (inm)) && + ('"' == inm[0]) && + ('"' == inm[strlen (inm) - 1]) ) { struct GNUNET_HashCode inm_h; if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (inm, - strlen (inm), + GNUNET_STRINGS_string_to_data (inm + 1, + strlen (inm) - 2, &inm_h, sizeof (inm_h))) { diff --git a/src/backend/anastasis-httpd_policy.h b/src/backend/anastasis-httpd_policy.h index 33d51cf..2735db6 100644 --- a/src/backend/anastasis-httpd_policy.h +++ b/src/backend/anastasis-httpd_policy.h @@ -1,6 +1,6 @@ /* This file is part of Anastasis - Copyright (C) 2014, 2015, 2016 Anastasis SARL + Copyright (C) 2019-2021 Anastasis SARL Anastasis 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 diff --git a/src/backend/anastasis-httpd_terms.c b/src/backend/anastasis-httpd_terms.c index b4debe7..94a6380 100644 --- a/src/backend/anastasis-httpd_terms.c +++ b/src/backend/anastasis-httpd_terms.c @@ -3,7 +3,7 @@ Copyright (C) 2020 Anastasis SARL 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 + 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. Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY @@ -36,13 +36,6 @@ static struct TALER_MHD_Legal *tos; static struct TALER_MHD_Legal *pp; -/** - * Manages a /terms call. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @return MHD result code - */ MHD_RESULT AH_handler_terms (struct AH_RequestHandler *rh, struct MHD_Connection *connection) @@ -53,15 +46,8 @@ AH_handler_terms (struct AH_RequestHandler *rh, } -/** - * Handle a "/privacy" request. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @return MHD result code - */ MHD_RESULT -AH_handler_privacy (const struct AH_RequestHandler *rh, +AH_handler_privacy (struct AH_RequestHandler *rh, struct MHD_Connection *connection) { (void) rh; @@ -70,11 +56,6 @@ AH_handler_privacy (const struct AH_RequestHandler *rh, } -/** - * Load our terms of service as per configuration. - * - * @param cfg configuration to process - */ void AH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) { diff --git a/src/backend/anastasis-httpd_terms.h b/src/backend/anastasis-httpd_terms.h index e34c86e..5043c57 100644 --- a/src/backend/anastasis-httpd_terms.h +++ b/src/backend/anastasis-httpd_terms.h @@ -45,7 +45,7 @@ AH_handler_terms (struct AH_RequestHandler *rh, * @return MHD result code */ MHD_RESULT -AH_handler_privacy (const struct AH_RequestHandler *rh, +AH_handler_privacy (struct AH_RequestHandler *rh, struct MHD_Connection *connection); /** diff --git a/src/backend/anastasis-httpd_truth.c b/src/backend/anastasis-httpd_truth-challenge.c index 4dd3ddc..a7d138f 100644 --- a/src/backend/anastasis-httpd_truth.c +++ b/src/backend/anastasis-httpd_truth-challenge.c @@ -1,6 +1,6 @@ /* This file is part of Anastasis - Copyright (C) 2019, 2021 Anastasis SARL + Copyright (C) 2019-2022 Anastasis SARL Anastasis 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 @@ -14,8 +14,8 @@ Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file anastasis-httpd_truth.c - * @brief functions to handle incoming requests on /truth + * @file anastasis-httpd_truth-challenge.c + * @brief functions to handle incoming requests on /truth/$TID/challenge * @author Dennis Neufeld * @author Dominik Meister * @author Christian Grothoff @@ -29,6 +29,7 @@ #include "anastasis_authorization_lib.h" #include <taler/taler_merchant_service.h> #include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> /** * What is the maximum frequency at which we allow @@ -38,18 +39,17 @@ GNUNET_TIME_UNIT_SECONDS, 30) /** - * How long do we hold an HTTP client connection if - * we are awaiting payment before giving up? - */ -#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_MINUTES, 30) - -/** * How long should the wallet check for auto-refunds before giving up? */ #define AUTO_REFUND_TIMEOUT GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MINUTES, 2) +/** + * How long should the wallet check for payment before giving up? + */ +#define PAYMENT_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 15) + /** * How many retries do we allow per code? @@ -57,7 +57,7 @@ #define INITIAL_RETRY_COUNTER 3 -struct GetContext +struct ChallengeContext { /** @@ -86,14 +86,24 @@ struct GetContext struct TM_HandlerContext *hc; /** + * Opaque parsing context. + */ + void *opaque_post_parsing_context; + + /** + * Uploaded JSON data, NULL if upload is not yet complete. + */ + json_t *root; + + /** * Kept in DLL for shutdown handling while suspended. */ - struct GetContext *next; + struct ChallengeContext *next; /** * Kept in DLL for shutdown handling while suspended. */ - struct GetContext *prev; + struct ChallengeContext *prev; /** * Connection handle for closing or resuming @@ -131,13 +141,7 @@ struct GetContext struct GNUNET_CONTAINER_HeapNode *hn; /** - * Challenge response we got from the request. - */ - struct GNUNET_HashCode challenge_response; - - /** - * How long do we wait at most for payment or - * authorization? + * When should this request time out? */ struct GNUNET_TIME_Absolute timeout; @@ -152,9 +156,9 @@ struct GetContext unsigned int response_code; /** - * true if client provided a payment secret / order ID? + * true if client did not provide a payment secret / order ID. */ - bool payment_identifier_provided; + bool no_payment_identifier_provided; /** * True if this entry is in the #gc_head DLL. @@ -166,13 +170,9 @@ struct GetContext */ bool suspended; - /** - * Did the request include a response? - */ - bool have_response; - }; + /** * Information we track for refunds. */ @@ -223,12 +223,12 @@ static struct RefundEntry *re_tail; /** * Head of linked list over all authorization processes */ -static struct GetContext *gc_head; +static struct ChallengeContext *gc_head; /** * Tail of linked list over all authorization processes */ -static struct GetContext *gc_tail; +static struct ChallengeContext *gc_tail; /** * Task running #do_timeout(). @@ -237,6 +237,27 @@ static struct GNUNET_SCHEDULER_Task *to_task; /** + * Generate a response telling the client that answering this + * challenge failed because the rate limit has been exceeded. + * + * @param gc request to answer for + * @return MHD status code + */ +static MHD_RESULT +reply_rate_limited (const struct ChallengeContext *gc) +{ + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_MHD_PACK_EC (TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED), + GNUNET_JSON_pack_uint64 ("request_limit", + gc->authorization->retry_counter), + GNUNET_JSON_pack_time_rel ("request_frequency", + gc->authorization->code_rotation_period)); +} + + +/** * Timeout requests that are past their due date. * * @param cls NULL @@ -244,7 +265,7 @@ static struct GNUNET_SCHEDULER_Task *to_task; static void do_timeout (void *cls) { - struct GetContext *gc; + struct ChallengeContext *gc; (void) cls; to_task = NULL; @@ -276,9 +297,9 @@ do_timeout (void *cls) void -AH_truth_shutdown (void) +AH_truth_challenge_shutdown (void) { - struct GetContext *gc; + struct ChallengeContext *gc; struct RefundEntry *re; while (NULL != (re = re_head)) @@ -339,22 +360,17 @@ AH_truth_shutdown (void) * Callback to process a POST /orders/ID/refund request * * @param cls closure with a `struct RefundEntry *` - * @param hr HTTP response details - * @param taler_refund_uri the refund uri offered to the wallet - * @param h_contract hash of the contract a Browser may need to authorize - * obtaining the HTTP response. + * @param rr response details */ static void refund_cb ( void *cls, - const struct TALER_MERCHANT_HttpResponse *hr, - const char *taler_refund_uri, - const struct GNUNET_HashCode *h_contract) + const struct TALER_MERCHANT_RefundResponse *rr) { struct RefundEntry *re = cls; re->ro = NULL; - switch (hr->http_status) + switch (rr->hr.http_status) { case MHD_HTTP_OK: { @@ -386,9 +402,9 @@ refund_cb ( GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Refund `%s' failed with HTTP status %u: %s (#%u)\n", re->order_id, - hr->http_status, - hr->hint, - (unsigned int) hr->ec); + rr->hr.http_status, + rr->hr.hint, + (unsigned int) rr->hr.ec); break; } GNUNET_CONTAINER_DLL_remove (re_head, @@ -405,7 +421,7 @@ refund_cb ( * @param gc request where we failed and should now grant a refund for */ static void -begin_refund (const struct GetContext *gc) +begin_refund (const struct ChallengeContext *gc) { struct RefundEntry *re; @@ -447,7 +463,7 @@ begin_refund (const struct GetContext *gc) static void request_done (struct TM_HandlerContext *hc) { - struct GetContext *gc = hc->ctx; + struct ChallengeContext *gc = hc->ctx; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Request completed\n"); @@ -484,6 +500,12 @@ request_done (struct TM_HandlerContext *hc) TALER_MERCHANT_orders_post_cancel (gc->po); gc->po = NULL; } + if (NULL != gc->root) + { + json_decref (gc->root); + gc->root = NULL; + } + TALER_MHD_parse_post_cleanup_callback (gc->opaque_post_parsing_context); GNUNET_free (gc); hc->ctx = NULL; } @@ -495,7 +517,7 @@ request_done (struct TM_HandlerContext *hc) * @param gc context to make payment request for */ static void -make_payment_request (struct GetContext *gc) +make_payment_request (struct ChallengeContext *gc) { struct MHD_Response *resp; @@ -559,14 +581,14 @@ make_payment_request (struct GetContext *gc) * Callbacks of this type are used to serve the result of submitting a * /contract request to a merchant. * - * @param cls our `struct GetContext` + * @param cls our `struct ChallengeContext` * @param por response details */ static void proposal_cb (void *cls, const struct TALER_MERCHANT_PostOrdersReply *por) { - struct GetContext *gc = cls; + struct ChallengeContext *gc = cls; enum GNUNET_DB_QueryStatus qs; gc->po = NULL; @@ -622,17 +644,16 @@ proposal_cb (void *cls, /** * Callback to process a GET /check-payment request * - * @param cls our `struct GetContext` - * @param hr HTTP response details + * @param cls our `struct ChallengeContext` * @param osr order status */ static void check_payment_cb (void *cls, - const struct TALER_MERCHANT_HttpResponse *hr, const struct TALER_MERCHANT_OrderStatusResponse *osr) { - struct GetContext *gc = cls; + struct ChallengeContext *gc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; gc->cpo = NULL; GNUNET_assert (gc->in_list); @@ -686,7 +707,8 @@ check_payment_cb (void *cls, } } - switch (osr->status) + GNUNET_assert (MHD_HTTP_OK == hr->http_status); + switch (osr->details.ok.status) { case TALER_MERCHANT_OSC_PAID: { @@ -729,7 +751,7 @@ check_payment_cb (void *cls, * @return MHD status code */ static MHD_RESULT -begin_payment (struct GetContext *gc) +begin_payment (struct ChallengeContext *gc) { enum GNUNET_DB_QueryStatus qs; char *order_id; @@ -769,7 +791,6 @@ begin_payment (struct GetContext *gc) AH_backend_url, order_id, NULL /* NOT session-bound */, - false, timeout, &check_payment_cb, gc); @@ -777,8 +798,9 @@ begin_payment (struct GetContext *gc) else { /* Create a fresh order */ + static const char *no_uuids[1] = { NULL }; json_t *order; - struct GNUNET_TIME_Absolute pay_deadline; + struct GNUNET_TIME_Timestamp pay_deadline; GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &gc->payment_identifier, @@ -789,9 +811,8 @@ begin_payment (struct GetContext *gc) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Creating fresh order `%s'\n", order_id); - pay_deadline = GNUNET_TIME_relative_to_absolute ( + pay_deadline = GNUNET_TIME_relative_to_timestamp ( ANASTASIS_CHALLENGE_OFFER_LIFETIME); - GNUNET_TIME_round_abs (&pay_deadline); order = GNUNET_JSON_PACK ( TALER_JSON_pack_amount ("amount", &gc->challenge_cost), @@ -801,8 +822,8 @@ begin_payment (struct GetContext *gc) order_id), GNUNET_JSON_pack_time_rel ("auto_refund", AUTO_REFUND_TIMEOUT), - GNUNET_JSON_pack_time_abs ("pay_deadline", - pay_deadline)); + GNUNET_JSON_pack_timestamp ("pay_deadline", + pay_deadline)); gc->po = TALER_MERCHANT_orders_post2 (AH_ctx, AH_backend_url, order, @@ -811,7 +832,7 @@ begin_payment (struct GetContext *gc) 0, NULL, /* no inventory products */ 0, - NULL, /* no uuids */ + no_uuids, /* no uuids */ false, /* do NOT require claim token */ &proposal_cb, gc); @@ -824,59 +845,33 @@ begin_payment (struct GetContext *gc) /** - * Load encrypted keyshare from db and return it to the client. + * Mark @a gc as suspended and update the respective + * data structures and jobs. * - * @param truth_uuid UUID to the truth for the looup - * @param connection the connection to respond upon - * @return MHD status code + * @param[in,out] gc context of the suspended operation */ -static MHD_RESULT -return_key_share ( - const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, - struct MHD_Connection *connection) +static void +gc_suspended (struct ChallengeContext *gc) { - struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; - + gc->suspended = true; + if (NULL == AH_to_heap) + AH_to_heap = GNUNET_CONTAINER_heap_create ( + GNUNET_CONTAINER_HEAP_ORDER_MIN); + gc->hn = GNUNET_CONTAINER_heap_insert (AH_to_heap, + gc, + gc->timeout.abs_value_us); + if (NULL != to_task) { - enum GNUNET_DB_QueryStatus qs; - - qs = db->get_key_share (db->cls, - truth_uuid, - &encrypted_keyshare); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get key share"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_ANASTASIS_TRUTH_KEY_SHARE_GONE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } + GNUNET_SCHEDULER_cancel (to_task); + to_task = NULL; } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning key share\n"); { - struct MHD_Response *resp; - MHD_RESULT ret; - - resp = MHD_create_response_from_buffer (sizeof (encrypted_keyshare), - &encrypted_keyshare, - MHD_RESPMEM_MUST_COPY); - TALER_MHD_add_global_headers (resp); - ret = MHD_queue_response (connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return ret; + struct ChallengeContext *rn; + + rn = GNUNET_CONTAINER_heap_peek (AH_to_heap); + to_task = GNUNET_SCHEDULER_add_at (rn->timeout, + &do_timeout, + NULL); } } @@ -890,21 +885,30 @@ return_key_share ( */ static MHD_RESULT run_authorization_process (struct MHD_Connection *connection, - struct GetContext *gc) + struct ChallengeContext *gc) { - enum ANASTASIS_AUTHORIZATION_Result ret; + enum ANASTASIS_AUTHORIZATION_ChallengeResult ret; enum GNUNET_DB_QueryStatus qs; GNUNET_assert (! gc->suspended); - ret = gc->authorization->process (gc->as, - gc->timeout, - connection); + if (NULL == gc->authorization->challenge) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "challenge method not implemented for authorization method"); + } + ret = gc->authorization->challenge (gc->as, + connection); switch (ret) { - case ANASTASIS_AUTHORIZATION_RES_SUCCESS: + case ANASTASIS_AUTHORIZATION_CRES_SUCCESS: /* Challenge sent successfully */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Authorization request sent successfully\n"); + "Authorization request %llu for %s sent successfully\n", + (unsigned long long) gc->code, + TALER_B2S (&gc->truth_uuid)); qs = db->mark_challenge_sent (db->cls, &gc->payment_identifier, &gc->truth_uuid, @@ -913,38 +917,19 @@ run_authorization_process (struct MHD_Connection *connection, gc->authorization->cleanup (gc->as); gc->as = NULL; return MHD_YES; - case ANASTASIS_AUTHORIZATION_RES_FAILED: - if (gc->payment_identifier_provided) + case ANASTASIS_AUTHORIZATION_CRES_FAILED: + if (! gc->no_payment_identifier_provided) { begin_refund (gc); } gc->authorization->cleanup (gc->as); gc->as = NULL; return MHD_YES; - case ANASTASIS_AUTHORIZATION_RES_SUSPENDED: + case ANASTASIS_AUTHORIZATION_CRES_SUSPENDED: /* connection was suspended */ - gc->suspended = true; - if (NULL == AH_to_heap) - AH_to_heap = GNUNET_CONTAINER_heap_create ( - GNUNET_CONTAINER_HEAP_ORDER_MIN); - gc->hn = GNUNET_CONTAINER_heap_insert (AH_to_heap, - gc, - gc->timeout.abs_value_us); - if (NULL != to_task) - { - GNUNET_SCHEDULER_cancel (to_task); - to_task = NULL; - } - { - struct GetContext *rn; - - rn = GNUNET_CONTAINER_heap_peek (AH_to_heap); - to_task = GNUNET_SCHEDULER_add_at (rn->timeout, - &do_timeout, - NULL); - } + gc_suspended (gc); return MHD_YES; - case ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED: + case ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED: /* Challenge sent successfully */ qs = db->mark_challenge_sent (db->cls, &gc->payment_identifier, @@ -954,24 +939,10 @@ run_authorization_process (struct MHD_Connection *connection, gc->authorization->cleanup (gc->as); gc->as = NULL; return MHD_NO; - case ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED: + case ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED: gc->authorization->cleanup (gc->as); gc->as = NULL; return MHD_NO; - case ANASTASIS_AUTHORIZATION_RES_FINISHED: - GNUNET_assert (! gc->suspended); - gc->authorization->cleanup (gc->as); - gc->as = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming with authorization successful!\n"); - if (gc->in_list) - { - GNUNET_CONTAINER_DLL_remove (gc_head, - gc_tail, - gc); - gc->in_list = false; - } - return MHD_YES; } GNUNET_break (0); return MHD_NO; @@ -979,141 +950,31 @@ run_authorization_process (struct MHD_Connection *connection, MHD_RESULT -AH_handler_truth_get ( +AH_handler_truth_challenge ( struct MHD_Connection *connection, + struct TM_HandlerContext *hc, const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, - struct TM_HandlerContext *hc) + const char *upload_data, + size_t *upload_data_size) { - struct GetContext *gc = hc->ctx; + struct ChallengeContext *gc = hc->ctx; void *encrypted_truth; size_t encrypted_truth_size; void *decrypted_truth; size_t decrypted_truth_size; char *truth_mime = NULL; - bool is_question; if (NULL == gc) { /* Fresh request, do initial setup */ - gc = GNUNET_new (struct GetContext); + gc = GNUNET_new (struct ChallengeContext); gc->hc = hc; hc->ctx = gc; gc->connection = connection; gc->truth_uuid = *truth_uuid; gc->hc->cc = &request_done; - { - const char *pay_id; - - pay_id = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); - if (NULL != pay_id) - { - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - pay_id, - strlen (pay_id), - &gc->payment_identifier, - sizeof (struct ANASTASIS_PaymentSecretP))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ANASTASIS_HTTP_HEADER_PAYMENT_IDENTIFIER); - } - gc->payment_identifier_provided = true; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client provided payment identifier `%s'\n", - pay_id); - } - } - - { - /* check if header contains Truth-Decryption-Key */ - const char *tdk; - - tdk = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); - if (NULL == tdk) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); - } - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data ( - tdk, - strlen (tdk), - &gc->truth_key, - sizeof (struct ANASTASIS_CRYPTO_TruthKeyP))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ANASTASIS_HTTP_HEADER_TRUTH_DECRYPTION_KEY); - } - } - - { - const char *challenge_response_s; - - challenge_response_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "response"); - if ( (NULL != challenge_response_s) && - (GNUNET_OK != - GNUNET_CRYPTO_hash_from_string (challenge_response_s, - &gc->challenge_response)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "response"); - } - gc->have_response = (NULL != challenge_response_s); - } - - { - const char *long_poll_timeout_ms; - - long_poll_timeout_ms = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != long_poll_timeout_ms) - { - unsigned int timeout; - char dummy; - - if (1 != sscanf (long_poll_timeout_ms, - "%u%c", - &timeout, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms (must be non-negative number)"); - } - gc->timeout - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_MILLISECONDS, - timeout)); - } - else - { - gc->timeout = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_UNIT_SECONDS); - } - } - + gc->timeout = GNUNET_TIME_relative_to_absolute ( + PAYMENT_TIMEOUT); } /* end of first-time initialization (if NULL == gc) */ else { @@ -1149,6 +1010,61 @@ AH_handler_truth_get ( was indeed paid! */ } + /* parse byte stream upload into JSON */ + if (NULL == gc->root) + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_post_json (connection, + &gc->opaque_post_parsing_context, + upload_data, + upload_data_size, + &gc->root); + if (GNUNET_SYSERR == res) + { + GNUNET_assert (NULL == gc->root); + return MHD_NO; /* bad upload, could not even generate error */ + } + if ( (GNUNET_NO == res) || + (NULL == gc->root) ) + { + GNUNET_assert (NULL == gc->root); + return MHD_YES; /* so far incomplete upload or parser error */ + } + + /* 'root' is now initialized */ + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("truth_decryption_key", + &gc->truth_key), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &gc->payment_identifier), + &gc->no_payment_identifier_provided), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + gc->root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + if (! gc->no_payment_identifier_provided) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client provided payment identifier `%s'\n", + TALER_B2S (&gc->payment_identifier)); + } + } + { /* load encrypted truth from DB */ enum GNUNET_DB_QueryStatus qs; @@ -1177,39 +1093,58 @@ AH_handler_truth_get ( case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - is_question = (0 == strcmp ("question", - method)); - if (! is_question) + if (0 == strcmp ("question", + method)) { - gc->authorization - = ANASTASIS_authorization_plugin_load (method, - db, - AH_cfg); - if (NULL == gc->authorization) - { - MHD_RESULT ret; + GNUNET_break_op (0); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD, + "question"); + } - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, - method); - GNUNET_free (encrypted_truth); - GNUNET_free (truth_mime); - GNUNET_free (method); - return ret; - } - gc->challenge_cost = gc->authorization->cost; + gc->authorization + = ANASTASIS_authorization_plugin_load (method, + db, + AH_cfg); + if (NULL == gc->authorization) + { + MHD_RESULT ret; + + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; } - else + + if (gc->authorization->user_provided_code) { - gc->challenge_cost = AH_question_cost; + MHD_RESULT ret; + + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; } + + gc->challenge_cost = gc->authorization->cost; GNUNET_free (method); } - if ( (is_question) || - (! gc->authorization->payment_plugin_managed) ) + if (! gc->authorization->payment_plugin_managed) { if (! TALER_amount_is_zero (&gc->challenge_cost)) { @@ -1217,7 +1152,7 @@ AH_handler_truth_get ( enum GNUNET_DB_QueryStatus qs; bool paid; - if (! gc->payment_identifier_provided) + if (gc->no_payment_identifier_provided) { GNUNET_free (truth_mime); GNUNET_free (encrypted_truth); @@ -1282,246 +1217,86 @@ AH_handler_truth_get ( { GNUNET_free (truth_mime); return TALER_MHD_reply_with_error (connection, - MHD_HTTP_EXPECTATION_FAILED, + MHD_HTTP_CONFLICT, TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, NULL); } - /* Special case for secure question: we do not generate a numeric challenge, - but check that the hash matches */ - if (is_question) + /* Not security question and no answer: use plugin to check if + decrypted truth is a valid challenge! */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No challenge provided, creating fresh challenge\n"); { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling security question challenge\n"); - if (! gc->have_response) - { - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, - NULL); - } + enum GNUNET_GenericReturnValue ret; + ret = gc->authorization->validate (gc->authorization->cls, + connection, + truth_mime, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (truth_mime); + switch (ret) { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Absolute rt; - uint64_t code; - enum ANASTASIS_DB_CodeStatus cs; - struct GNUNET_HashCode hc; - bool satisfied; - uint64_t dummy; - - rt = GNUNET_TIME_UNIT_FOREVER_ABS; - qs = db->create_challenge_code (db->cls, - &gc->truth_uuid, - MAX_QUESTION_FREQ, - GNUNET_TIME_UNIT_HOURS, - INITIAL_RETRY_COUNTER, - &rt, - &code); - if (0 > qs) - { - GNUNET_break (0 < qs); - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "create_challenge_code (for rate limiting)"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, - NULL); - } - /* decrement trial counter */ - ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ - &hc); - cs = db->verify_challenge_code (db->cls, - &gc->truth_uuid, - &hc, - &dummy, - &satisfied); - switch (cs) - { - case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: - /* good, what we wanted */ - break; - case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: - case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (gc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "verify_challenge_code"); - case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, - NULL); - case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: - /* this should be impossible, we used code+1 */ - GNUNET_assert (0); - } - } - if ( (decrypted_truth_size != sizeof (struct GNUNET_HashCode)) || - (0 != memcmp (&gc->challenge_response, - decrypted_truth, - decrypted_truth_size)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Wrong answer provided to secure question had %u bytes, wanted %u\n", - (unsigned int) decrypted_truth_size, - (unsigned int) sizeof (struct GNUNET_HashCode)); + case GNUNET_OK: + /* data valid, continued below */ + break; + case GNUNET_NO: + /* data invalid, reply was queued */ GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, - NULL); + return MHD_YES; + case GNUNET_SYSERR: + /* data invalid, reply was NOT queued */ + GNUNET_free (decrypted_truth); + return MHD_NO; } - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return return_key_share (&gc->truth_uuid, - connection); } - /* Not security question, check for answer in DB */ - if (gc->have_response) + /* Setup challenge and begin authorization process */ { - enum ANASTASIS_DB_CodeStatus cs; - bool satisfied; - uint64_t code; + struct GNUNET_TIME_Timestamp transmission_date; + enum GNUNET_DB_QueryStatus qs; - GNUNET_free (truth_mime); - cs = db->verify_challenge_code (db->cls, + qs = db->create_challenge_code (db->cls, &gc->truth_uuid, - &gc->challenge_response, - &code, - &satisfied); - switch (cs) + gc->authorization->code_rotation_period, + gc->authorization->code_validity_period, + gc->authorization->retry_counter, + &transmission_date, + &gc->code); + switch (qs) { - case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Provided response does not match our stored challenge\n"); - GNUNET_free (decrypted_truth); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, - NULL); - case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: - case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); GNUNET_free (decrypted_truth); return TALER_MHD_reply_with_error (gc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "verify_challenge_code"); - case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Response code unknown (possibly expired). Testing if we may provide a new one.\n"); - gc->have_response = false; - break; - case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + "create_challenge_code"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* 0 == retry_counter of existing challenge => rate limit exceeded */ + GNUNET_free (decrypted_truth); + return reply_rate_limited (gc); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* challenge code was stored successfully*/ GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Response code valid (%s)\n", - satisfied ? "satisfied" : "unsatisfied"); - if (satisfied) - { - GNUNET_free (decrypted_truth); - return return_key_share (&gc->truth_uuid, - connection); - } - /* continue with authorization plugin below */ - gc->code = code; + "Created fresh challenge\n"); break; - default: - GNUNET_break (0); - return MHD_NO; } - } - if (! gc->have_response) - { - /* Not security question and no answer: use plugin to check if - decrypted truth is a valid challenge! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No challenge provided, creating fresh challenge\n"); - { - enum GNUNET_GenericReturnValue ret; - ret = gc->authorization->validate (gc->authorization->cls, - connection, - truth_mime, - decrypted_truth, - decrypted_truth_size); - GNUNET_free (truth_mime); - switch (ret) - { - case GNUNET_OK: - /* data valid, continued below */ - break; - case GNUNET_NO: - /* data invalid, reply was queued */ - GNUNET_free (decrypted_truth); - return MHD_YES; - case GNUNET_SYSERR: - /* data invalid, reply was NOT queued */ - GNUNET_free (decrypted_truth); - return MHD_NO; - } - } - - /* Setup challenge and begin authorization process */ + if (GNUNET_TIME_relative_cmp ( + GNUNET_TIME_absolute_get_duration ( + transmission_date.abs_time), + <, + gc->authorization->code_retransmission_frequency) ) { - struct GNUNET_TIME_Absolute transmission_date; - enum GNUNET_DB_QueryStatus qs; - - qs = db->create_challenge_code (db->cls, - &gc->truth_uuid, - gc->authorization->code_rotation_period, - gc->authorization->code_validity_period, - gc->authorization->retry_counter, - &transmission_date, - &gc->code); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - GNUNET_free (decrypted_truth); - return TALER_MHD_reply_with_error (gc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "create_challenge_code"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* 0 == retry_counter of existing challenge => rate limit exceeded */ - GNUNET_free (decrypted_truth); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* challenge code was stored successfully*/ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Created fresh challenge\n"); - break; - } - - if (GNUNET_TIME_absolute_get_duration (transmission_date).rel_value_us < - gc->authorization->code_retransmission_frequency.rel_value_us) - { - /* Too early for a retransmission! */ - GNUNET_free (decrypted_truth); - return TALER_MHD_reply_with_error (gc->connection, - MHD_HTTP_ALREADY_REPORTED, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_ACTIVE, - NULL); - } + /* Too early for a retransmission! */ + GNUNET_free (decrypted_truth); + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("challenge_type", + "TAN_ALREADY_SENT")); } } diff --git a/src/backend/anastasis-httpd_truth-solve.c b/src/backend/anastasis-httpd_truth-solve.c new file mode 100644 index 0000000..eb09dc7 --- /dev/null +++ b/src/backend/anastasis-httpd_truth-solve.c @@ -0,0 +1,1474 @@ +/* + This file is part of Anastasis + Copyright (C) 2019-2022 Anastasis SARL + + Anastasis 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. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_truth-solve.c + * @brief functions to handle incoming requests on /truth/$TID/solve + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include "anastasis_authorization_lib.h" +#include <taler/taler_merchant_service.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> + +/** + * What is the maximum frequency at which we allow + * clients to attempt to answer security questions? + */ +#define MAX_QUESTION_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * How long should the wallet check for auto-refunds before giving up? + */ +#define AUTO_REFUND_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + + +/** + * How many retries do we allow per code? + */ +#define INITIAL_RETRY_COUNTER 3 + + +struct SolveContext +{ + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Key to decrypt the truth. + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * Cost for paying the challenge. + */ + struct TALER_Amount challenge_cost; + + /** + * Our handler context. + */ + struct TM_HandlerContext *hc; + + /** + * Opaque parsing context. + */ + void *opaque_post_parsing_context; + + /** + * Uploaded JSON data, NULL if upload is not yet complete. + */ + json_t *root; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct SolveContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct SolveContext *prev; + + /** + * Connection handle for closing or resuming + */ + struct MHD_Connection *connection; + + /** + * Reference to the authorization plugin which was loaded + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * Status of the authorization + */ + struct ANASTASIS_AUTHORIZATION_State *as; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * Our entry in the #to_heap, or NULL. + */ + struct GNUNET_CONTAINER_HeapNode *hn; + + /** + * Challenge response we got from the request. + */ + struct GNUNET_HashCode challenge_response; + + /** + * How long do we wait at most for payment or + * authorization? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Random authorization code we are using. + */ + uint64_t code; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * true if client did not provide a payment secret / order ID. + */ + bool no_payment_identifier_provided; + + /** + * True if this entry is in the #gc_head DLL. + */ + bool in_list; + + /** + * True if this entry is currently suspended. + */ + bool suspended; + +}; + + +/** + * Head of linked list over all authorization processes + */ +static struct SolveContext *gc_head; + +/** + * Tail of linked list over all authorization processes + */ +static struct SolveContext *gc_tail; + +/** + * Task running #do_timeout(). + */ +static struct GNUNET_SCHEDULER_Task *to_task; + + +/** + * Generate a response telling the client that answering this + * challenge failed because the rate limit has been exceeded. + * + * @param gc request to answer for + * @return MHD status code + */ +static MHD_RESULT +reply_rate_limited (const struct SolveContext *gc) +{ + if (NULL != gc->authorization) + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_MHD_PACK_EC (TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED), + GNUNET_JSON_pack_uint64 ("request_limit", + gc->authorization->retry_counter), + GNUNET_JSON_pack_time_rel ("request_frequency", + gc->authorization->code_rotation_period)); + /* must be security question */ + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_MHD_PACK_EC (TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED), + GNUNET_JSON_pack_uint64 ("request_limit", + INITIAL_RETRY_COUNTER), + GNUNET_JSON_pack_time_rel ("request_frequency", + MAX_QUESTION_FREQ)); +} + + +/** + * Timeout requests that are past their due date. + * + * @param cls NULL + */ +static void +do_timeout (void *cls) +{ + struct SolveContext *gc; + + (void) cls; + to_task = NULL; + while (NULL != + (gc = GNUNET_CONTAINER_heap_peek (AH_to_heap))) + { + if (GNUNET_TIME_absolute_is_future (gc->timeout)) + break; + if (gc->suspended) + { + /* Test needed as we may have a "concurrent" + wakeup from another task that did not clear + this entry from the heap before the + response process concluded. */ + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + GNUNET_assert (NULL != gc->hn); + gc->hn = NULL; + GNUNET_assert (gc == + GNUNET_CONTAINER_heap_remove_root (AH_to_heap)); + } + if (NULL == gc) + return; + to_task = GNUNET_SCHEDULER_add_at (gc->timeout, + &do_timeout, + NULL); +} + + +void +AH_truth_solve_shutdown (void) +{ + struct SolveContext *gc; + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (gc->suspended) + { + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->as = NULL; + gc->authorization = NULL; + } + } + ANASTASIS_authorization_plugin_shutdown (); + if (NULL != to_task) + { + GNUNET_SCHEDULER_cancel (to_task); + to_task = NULL; + } +} + + +/** + * Callback used to notify the application about completed requests. + * Cleans up the requests data structures. + * + * @param[in,out] hc + */ +static void +request_done (struct TM_HandlerContext *hc) +{ + struct SolveContext *gc = hc->ctx; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request completed\n"); + if (NULL == gc) + return; + hc->cc = NULL; + GNUNET_assert (! gc->suspended); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + if (NULL != gc->hn) + { + GNUNET_assert (gc == + GNUNET_CONTAINER_heap_remove_node (gc->hn)); + gc->hn = NULL; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->authorization = NULL; + gc->as = NULL; + } + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (NULL != gc->root) + { + json_decref (gc->root); + gc->root = NULL; + } + TALER_MHD_parse_post_cleanup_callback (gc->opaque_post_parsing_context); + GNUNET_free (gc); + hc->ctx = NULL; +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param gc context to make payment request for + */ +static void +make_payment_request (struct SolveContext *gc) +{ + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *order_id; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request `%s'\n", + hdr); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + gc->resp = resp; + gc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls our `struct SolveContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct SolveContext *gc = cls; + enum GNUNET_DB_QueryStatus qs; + + gc->po = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + gc->suspended = false; + MHD_resume_connection (gc->connection); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + gc->resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR), + GNUNET_JSON_pack_string ("hint", + "Failed to setup order with merchant backend"), + GNUNET_JSON_pack_uint64 ("backend-ec", + por->hr.ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + por->hr.http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("backend-reply", + (json_t *) por->hr.reply))); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + qs = db->record_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier, + &gc->challenge_cost); + if (0 >= qs) + { + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "record challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setup fresh order, creating payment request\n"); + make_payment_request (gc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct SolveContext` + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_OrderStatusResponse *osr) + +{ + struct SolveContext *gc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; + + gc->cpo = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + gc->suspended = false; + MHD_resume_connection (gc->connection); + AH_trigger_daemon (NULL); + + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; + case MHD_HTTP_NOT_FOUND: + /* We created this order before, how can it be not found now? */ + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_TRUTH_ORDER_DISAPPEARED, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_BAD_GATEWAY: + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_GATEWAY_TIMEOUT: + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + "Timeout check payment status"); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + return; + default: + { + char status[14]; + + GNUNET_snprintf (status, + sizeof (status), + "%u", + hr->http_status); + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS, + status); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + } + + GNUNET_assert (MHD_HTTP_OK == hr->http_status); + switch (osr->details.ok.status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->update_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (0 <= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order has been paid, continuing with request processing\n"); + return; /* continue as planned */ + } + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "update challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_CLAIMED: + case TALER_MERCHANT_OSC_UNPAID: + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order remains unpaid, sending payment request again\n"); + make_payment_request (gc); + return; + } + /* should never get here */ + GNUNET_break (0); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param gc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct SolveContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + char *order_id; + + qs = db->lookup_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup challenge payment"); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_tail, + gc_head, + gc); + GNUNET_assert (! gc->suspended); + gc->suspended = true; + MHD_suspend_connection (gc->connection); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* We already created the order, check if it was paid */ + struct GNUNET_TIME_Relative timeout; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order exists, checking payment status for order `%s'\n", + order_id); + timeout = GNUNET_TIME_absolute_get_remaining (gc->timeout); + gc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* NOT session-bound */, + timeout, + &check_payment_cb, + gc); + } + else + { + /* Create a fresh order */ + static const char *no_uuids[1] = { NULL }; + json_t *order; + struct GNUNET_TIME_Timestamp pay_deadline; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating fresh order `%s'\n", + order_id); + pay_deadline = GNUNET_TIME_relative_to_timestamp ( + ANASTASIS_CHALLENGE_OFFER_LIFETIME); + order = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &gc->challenge_cost), + GNUNET_JSON_pack_string ("summary", + "challenge fee for anastasis service"), + GNUNET_JSON_pack_string ("order_id", + order_id), + GNUNET_JSON_pack_time_rel ("auto_refund", + AUTO_REFUND_TIMEOUT), + GNUNET_JSON_pack_timestamp ("pay_deadline", + pay_deadline)); + gc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + AUTO_REFUND_TIMEOUT, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + no_uuids, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + gc); + json_decref (order); + } + GNUNET_free (order_id); + AH_trigger_curl (); + return MHD_YES; +} + + +/** + * Load encrypted keyshare from db and return it to the client. + * + * @param truth_uuid UUID to the truth for the looup + * @param connection the connection to respond upon + * @return MHD status code + */ +static MHD_RESULT +return_key_share ( + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct MHD_Connection *connection) +{ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning key share of %s\n", + TALER_B2S (truth_uuid)); + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->get_key_share (db->cls, + truth_uuid, + &encrypted_keyshare); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get key share"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* this should be "impossible", after all the + client was able to solve the challenge! + (Exception: we deleted the truth via GC + just while the client was trying to recover. + Alas, highly unlikely...) */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_KEY_SHARE_GONE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (sizeof (encrypted_keyshare), + &encrypted_keyshare, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +/** + * Mark @a gc as suspended and update the respective + * data structures and jobs. + * + * @param[in,out] gc context of the suspended operation + */ +static void +gc_suspended (struct SolveContext *gc) +{ + GNUNET_assert (NULL == gc->hn); + GNUNET_assert (! gc->suspended); + gc->suspended = true; + if (NULL == AH_to_heap) + AH_to_heap = GNUNET_CONTAINER_heap_create ( + GNUNET_CONTAINER_HEAP_ORDER_MIN); + gc->hn = GNUNET_CONTAINER_heap_insert (AH_to_heap, + gc, + gc->timeout.abs_value_us); + if (NULL != to_task) + { + GNUNET_SCHEDULER_cancel (to_task); + to_task = NULL; + } + { + struct SolveContext *rn; + + rn = GNUNET_CONTAINER_heap_peek (AH_to_heap); + to_task = GNUNET_SCHEDULER_add_at (rn->timeout, + &do_timeout, + NULL); + } +} + + +/** + * Run the authorization method-specific 'process' function and continue + * based on its result with generating an HTTP response. + * + * @param connection the connection we are handling + * @param gc our overall handler context + */ +static MHD_RESULT +run_authorization_process (struct MHD_Connection *connection, + struct SolveContext *gc) +{ + enum ANASTASIS_AUTHORIZATION_SolveResult ret; + + GNUNET_assert (! gc->suspended); + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + ret = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + connection); + switch (ret) + { + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + /* connection was suspended */ + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + GNUNET_assert (! gc->suspended); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming with authorization successful!\n"); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + return MHD_YES; + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * Use the database to rate-limit queries to the authentication + * procedure, but without actually storing 'real' challenge codes. + * + * @param[in,out] gc context to rate limit requests for + * @return #GNUNET_OK if rate-limiting passes, + * #GNUNET_NO if a reply was sent (rate limited) + * #GNUNET_SYSERR if we failed and no reply + * was queued + */ +static enum GNUNET_GenericReturnValue +rate_limit (struct SolveContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp rt; + uint64_t code; + enum ANASTASIS_DB_CodeStatus cs; + struct GNUNET_HashCode hc; + bool satisfied; + uint64_t dummy; + + rt = GNUNET_TIME_UNIT_FOREVER_TS; + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + MAX_QUESTION_FREQ, + GNUNET_TIME_UNIT_HOURS, + INITIAL_RETRY_COUNTER, + &rt, + &code); + if (0 > qs) + { + GNUNET_break (0 < qs); + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "create_challenge_code (for rate limiting)")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return (MHD_YES == + reply_rate_limited (gc)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Using intentionally wrong answer to produce rate-limiting\n"); + /* decrement trial counter */ + ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ + &hc); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &hc, + &dummy, + &satisfied); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + /* good, what we wanted */ + return GNUNET_OK; + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code")) + ? GNUNET_NO + : GNUNET_SYSERR; + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + return (MHD_YES == + reply_rate_limited (gc)) + ? GNUNET_NO + : GNUNET_SYSERR; + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + /* this should be impossible, we used code+1 */ + GNUNET_assert (0); + } + return GNUNET_SYSERR; +} + + +/** + * Handle special case of a security question where we do not + * generate a code. Rate limits answers against brute forcing. + * + * @param[in,out] gc request to handle + * @param decrypted_truth hash to check against + * @param decrypted_truth_size number of bytes in @a decrypted_truth + * @return MHD status code + */ +static MHD_RESULT +handle_security_question (struct SolveContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling security question challenge\n"); + /* rate limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = rate_limit (gc); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + /* check reply matches truth */ + if ( (decrypted_truth_size != sizeof (struct GNUNET_HashCode)) || + (0 != memcmp (&gc->challenge_response, + decrypted_truth, + decrypted_truth_size)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Wrong answer provided to secure question had %u bytes, wanted %u\n", + (unsigned int) decrypted_truth_size, + (unsigned int) sizeof (struct GNUNET_HashCode)); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + } + /* good, return the key share */ + return return_key_share (&gc->truth_uuid, + gc->connection); +} + + +/** + * Handle special case of an answer being directly checked by the + * plugin and not by our database. Also ensures that the + * request is rate-limited. + * + * @param[in,out] gc request to handle + * @param decrypted_truth hash to check against + * @param decrypted_truth_size number of bytes in @a decrypted_truth + * @return MHD status code + */ +static MHD_RESULT +direct_validation (struct SolveContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + /* Non-random code, call plugin directly! */ + enum ANASTASIS_AUTHORIZATION_SolveResult aar; + enum GNUNET_GenericReturnValue ret; + + ret = rate_limit (gc); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + 0LLU, + decrypted_truth, + decrypted_truth_size); + if (NULL == gc->as) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + NULL); + } + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + aar = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + gc->connection); + switch (aar) + { + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + return return_key_share (&gc->truth_uuid, + gc->connection); + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * Handle special case of an answer being checked + * by the plugin asynchronously (IBAN) after we inverted + * the hash using the database. + * + * @param[in,out] gc request to handle + * @param code validation code provided by the client + * @param decrypted_truth hash to check against + * @param decrypted_truth_size number of bytes in @a decrypted_truth + * @return MHD status code + */ +static MHD_RESULT +iban_validation (struct SolveContext *gc, + uint64_t code, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + enum ANASTASIS_AUTHORIZATION_SolveResult aar; + + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + code, + decrypted_truth, + decrypted_truth_size); + if (NULL == gc->as) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + NULL); + } + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + aar = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + gc->connection); + switch (aar) + { + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + return return_key_share (&gc->truth_uuid, + gc->connection); + } + GNUNET_break (0); + return MHD_NO; +} + + +MHD_RESULT +AH_handler_truth_solve ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *upload_data, + size_t *upload_data_size) +{ + struct SolveContext *gc = hc->ctx; + void *encrypted_truth; + size_t encrypted_truth_size; + void *decrypted_truth; + size_t decrypted_truth_size; + char *truth_mime = NULL; + bool is_question; + + if (NULL == gc) + { + /* Fresh request, do initial setup */ + gc = GNUNET_new (struct SolveContext); + gc->hc = hc; + hc->ctx = gc; + gc->connection = connection; + gc->truth_uuid = *truth_uuid; + gc->hc->cc = &request_done; + gc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + TALER_MHD_parse_request_timeout (connection, + &gc->timeout); + } /* end of first-time initialization (if NULL == gc) */ + else + { + /* might have been woken up by authorization plugin, + so clear the flag. MDH called us, so we are + clearly no longer suspended */ + gc->suspended = false; + if (NULL != gc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + ret = MHD_queue_response (connection, + gc->response_code, + gc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (gc->resp); + gc->resp = NULL; + return ret; + } + if (NULL != gc->as) + { + /* Authorization process is "running", check what is going on */ + GNUNET_assert (NULL != gc->authorization); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Continuing with running the authorization process\n"); + GNUNET_assert (! gc->suspended); + return run_authorization_process (connection, + gc); + } + /* We get here if the async check for payment said this request + was indeed paid! */ + } + + if (NULL == gc->root) + { + /* parse byte stream upload into JSON */ + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_post_json (connection, + &gc->opaque_post_parsing_context, + upload_data, + upload_data_size, + &gc->root); + if (GNUNET_SYSERR == res) + { + GNUNET_assert (NULL == gc->root); + return MHD_NO; /* bad upload, could not even generate error */ + } + if ( (GNUNET_NO == res) || + (NULL == gc->root) ) + { + GNUNET_assert (NULL == gc->root); + return MHD_YES; /* so far incomplete upload or parser error */ + } + + /* 'root' is now initialized, parse JSON body */ + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("truth_decryption_key", + &gc->truth_key), + GNUNET_JSON_spec_fixed_auto ("h_response", + &gc->challenge_response), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &gc->payment_identifier), + &gc->no_payment_identifier_provided), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + gc->root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + if (! gc->no_payment_identifier_provided) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client provided payment identifier `%s'\n", + TALER_B2S (&gc->payment_identifier)); + } + } + + { + /* load encrypted truth from DB; we may do this repeatedly + while handling the same request, if payment was checked + asynchronously! */ + enum GNUNET_DB_QueryStatus qs; + char *method; + + qs = db->get_escrow_challenge (db->cls, + &gc->truth_uuid, + &encrypted_truth, + &encrypted_truth_size, + &truth_mime, + &method); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get escrow challenge"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + is_question = (0 == strcmp ("question", + method)); + if (! is_question) + { + gc->authorization + = ANASTASIS_authorization_plugin_load (method, + db, + AH_cfg); + if (NULL == gc->authorization) + { + MHD_RESULT ret; + + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; + } + gc->challenge_cost = gc->authorization->cost; + } + else + { + gc->challenge_cost = AH_question_cost; + } + GNUNET_free (method); + } + + /* check for payment */ + if ( (is_question) || + (! gc->authorization->payment_plugin_managed) ) + { + if (! TALER_amount_is_zero (&gc->challenge_cost)) + { + /* Check database to see if the transaction is paid for */ + enum GNUNET_DB_QueryStatus qs; + bool paid; + + if (gc->no_payment_identifier_provided) + { + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning payment, client did not provide payment identifier\n"); + return begin_payment (gc); + } + qs = db->check_challenge_payment (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + &paid); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check challenge payment"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Create fresh payment identifier (cannot trust client) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client-provided payment identifier is unknown.\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (! paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment identifier known. Checking payment with client's payment identifier\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment confirmed\n"); + break; + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is free of charge\n"); + } + } + + /* We've been paid, now validate the response */ + /* decrypt encrypted_truth */ + ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); + GNUNET_free (encrypted_truth); + if (NULL == decrypted_truth) + { + /* most likely, the decryption key is simply wrong */ + GNUNET_break_op (0); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } + + /* Special case for secure question: we do not generate a numeric challenge, + but check that the hash matches */ + if (is_question) + { + MHD_RESULT ret; + + ret = handle_security_question (gc, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (truth_mime); + GNUNET_free (decrypted_truth); + return ret; + } + + /* Not security question, check for answer in DB */ + { + enum ANASTASIS_DB_CodeStatus cs; + bool satisfied = false; + uint64_t code; + + GNUNET_free (truth_mime); + if (gc->authorization->user_provided_code) + { + MHD_RESULT res; + + if (GNUNET_TIME_absolute_is_past (gc->timeout)) + { + GNUNET_free (decrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout with user provided code\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_IBAN_MISSING_TRANSFER, + "timeout awaiting validation"); + } + res = direct_validation (gc, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + return res; + } + + /* random code, check against database */ + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &gc->challenge_response, + &code, + &satisfied); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Provided response does not match our stored challenge\n"); + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code"); + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + GNUNET_free (decrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Specified challenge code %s was not issued\n", + GNUNET_h2s (&gc->challenge_response)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_UNKNOWN, + "specific challenge code was not issued"); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + if (! satisfied) + { + MHD_RESULT res; + + res = iban_validation (gc, + code, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + return res; + } + GNUNET_free (decrypted_truth); + return return_key_share (&gc->truth_uuid, + connection); + default: + GNUNET_break (0); + return MHD_NO; + } + } +} diff --git a/src/backend/anastasis-httpd_truth_upload.c b/src/backend/anastasis-httpd_truth-upload.c index d9e63c3..1c2a58d 100644 --- a/src/backend/anastasis-httpd_truth_upload.c +++ b/src/backend/anastasis-httpd_truth-upload.c @@ -287,15 +287,14 @@ proposal_cb (void *cls, * Callback to process a GET /check-payment request * * @param cls our `struct PolicyUploadContext` - * @param hr HTTP response details * @param osr order status */ static void check_payment_cb (void *cls, - const struct TALER_MERCHANT_HttpResponse *hr, const struct TALER_MERCHANT_OrderStatusResponse *osr) { struct TruthUploadContext *tuc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; tuc->cpo = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, @@ -311,7 +310,7 @@ check_payment_cb (void *cls, NULL); break; case MHD_HTTP_OK: - switch (osr->status) + switch (osr->details.ok.status) { case TALER_MERCHANT_OSC_PAID: { @@ -321,13 +320,12 @@ check_payment_cb (void *cls, const json_t *contract; struct TALER_Amount amount; struct GNUNET_JSON_Specification cspec[] = { - TALER_JSON_spec_amount ("amount", - AH_currency, - &amount), + TALER_JSON_spec_amount_any ("amount", + &amount), GNUNET_JSON_spec_end () }; - contract = osr->details.paid.contract_terms; + contract = osr->details.ok.details.paid.contract_terms; if (GNUNET_OK != GNUNET_JSON_parse (contract, cspec, @@ -356,7 +354,7 @@ check_payment_cb (void *cls, qs = db->record_truth_upload_payment ( db->cls, &tuc->truth_uuid, - &osr->details.paid.deposit_total, + &osr->details.ok.details.paid.deposit_total, paid_until); if (qs <= 0) { @@ -398,6 +396,7 @@ check_payment_cb (void *cls, case MHD_HTTP_NOT_FOUND: /* Setup fresh order */ { + static const char *no_uuids[1] = { NULL }; char *order_id; json_t *order; @@ -417,7 +416,6 @@ check_payment_cb (void *cls, "description", "challenge storage fee", "quantity", (json_int_t) tuc->years_to_pay, "unit", "years", - "order_id", order_id); GNUNET_free (order_id); @@ -429,7 +427,7 @@ check_payment_cb (void *cls, 0, NULL, /* no inventory products */ 0, - NULL, /* no uuids */ + no_uuids, /* no uuids */ false, /* do NOT require claim token */ &proposal_cb, tuc); @@ -487,7 +485,6 @@ begin_payment (struct TruthUploadContext *tuc) AH_backend_url, order_id, NULL /* our payments are NOT session-bound */, - false, timeout, &check_payment_cb, tuc); @@ -519,17 +516,18 @@ AH_handler_truth_post ( struct TruthUploadContext *tuc = hc->ctx; MHD_RESULT ret; int res; - struct ANASTASIS_CRYPTO_EncryptedKeyShareP keyshare_data; + struct ANASTASIS_CRYPTO_EncryptedKeyShareP key_share_data; void *encrypted_truth; size_t encrypted_truth_size; const char *truth_mime = NULL; const char *type; enum GNUNET_DB_QueryStatus qs; uint32_t storage_years; - struct GNUNET_TIME_Absolute paid_until; + struct GNUNET_TIME_Timestamp paid_until + = GNUNET_TIME_UNIT_ZERO_TS; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("keyshare_data", - &keyshare_data), + GNUNET_JSON_spec_fixed_auto ("key_share_data", + &key_share_data), GNUNET_JSON_spec_string ("type", &type), GNUNET_JSON_spec_varsize ("encrypted_truth", @@ -537,7 +535,8 @@ AH_handler_truth_post ( &encrypted_truth_size), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("truth_mime", - &truth_mime)), + &truth_mime), + NULL), GNUNET_JSON_spec_uint32 ("storage_duration_years", &storage_years), GNUNET_JSON_spec_end () @@ -550,78 +549,12 @@ AH_handler_truth_post ( tuc->truth_uuid = *truth_uuid; hc->ctx = tuc; hc->cc = &cleanup_truth_post; - - /* check for excessive upload */ - { - const char *lens; - unsigned long len; - char dummy; - - lens = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_CONTENT_LENGTH); - if ( (NULL == lens) || - (1 != sscanf (lens, - "%lu%c", - &len, - &dummy)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - (NULL == lens) - ? TALER_EC_ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH - : TALER_EC_ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH, - NULL); - } - if (len / 1024 / 1024 >= AH_upload_limit_mb) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_PAYLOAD_TOO_LARGE, - TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH, - "Content-length value not acceptable"); - } - } - - { - const char *long_poll_timeout_ms; - - long_poll_timeout_ms = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "timeout_ms"); - if (NULL != long_poll_timeout_ms) - { - unsigned int timeout; - char dummy; - - if (1 != sscanf (long_poll_timeout_ms, - "%u%c", - &timeout, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms (must be non-negative number)"); - } - tuc->timeout - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_MILLISECONDS, - timeout)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Long polling for %u ms enabled\n", - timeout); - } - else - { - tuc->timeout = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_UNIT_SECONDS); - } - } - + TALER_MHD_check_content_length (connection, + AH_upload_limit_mb * 1024LLU * 1024LLU); + tuc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + TALER_MHD_parse_request_timeout (connection, + &tuc->timeout); } /* end 'if (NULL == tuc)' */ if (NULL != tuc->resp) @@ -695,95 +628,89 @@ AH_handler_truth_post ( if (0 == storage_years) storage_years = 1; + if (! TALER_amount_is_zero (&AH_truth_upload_fee)) { - struct TALER_Amount zero_amount; - - TALER_amount_set_zero (AH_currency, - &zero_amount); - if (0 != TALER_amount_cmp (&AH_truth_upload_fee, - &zero_amount)) + struct GNUNET_TIME_Timestamp desired_until; + enum GNUNET_DB_QueryStatus qs; + + desired_until + = GNUNET_TIME_relative_to_timestamp ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + storage_years)); + qs = db->check_truth_upload_paid (db->cls, + truth_uuid, + &paid_until); + if (qs < 0) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + if ( (0 == qs) || + (GNUNET_TIME_timestamp_cmp (paid_until, + <, + desired_until) ) ) { - struct GNUNET_TIME_Absolute desired_until; - enum GNUNET_DB_QueryStatus qs; - - desired_until - = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, - storage_years)); - qs = db->check_truth_upload_paid (db->cls, - truth_uuid, - &paid_until); - if (qs < 0) + struct GNUNET_TIME_Relative rem; + + if (GNUNET_TIME_absolute_is_past (paid_until.abs_time)) + paid_until = GNUNET_TIME_timestamp_get (); + rem = GNUNET_TIME_absolute_get_difference (paid_until.abs_time, + desired_until.abs_time); + tuc->years_to_pay = rem.rel_value_us + / GNUNET_TIME_UNIT_YEARS.rel_value_us; + if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) + tuc->years_to_pay++; + if (0 > + TALER_amount_multiply (&tuc->upload_fee, + &AH_truth_upload_fee, + tuc->years_to_pay)) + { + GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - if ( (0 == qs) || - (paid_until.abs_value_us < desired_until.abs_value_us) ) + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "storage_duration_years"); + } + if (! TALER_amount_is_zero (&tuc->upload_fee)) { - struct GNUNET_TIME_Absolute now; - struct GNUNET_TIME_Relative rem; - - now = GNUNET_TIME_absolute_get (); - if (paid_until.abs_value_us < now.abs_value_us) - paid_until = now; - rem = GNUNET_TIME_absolute_get_difference (paid_until, - desired_until); - tuc->years_to_pay = rem.rel_value_us - / GNUNET_TIME_UNIT_YEARS.rel_value_us; - if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us)) - tuc->years_to_pay++; - if (0 > - TALER_amount_multiply (&tuc->upload_fee, - &AH_truth_upload_fee, - tuc->years_to_pay)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "storage_duration_years"); - } - if ( (0 != tuc->upload_fee.fraction) || - (0 != tuc->upload_fee.value) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Truth upload payment required (%d)!\n", - qs); - return begin_payment (tuc); - } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Truth upload payment required (%d)!\n", + qs); + return begin_payment (tuc); } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "TRUTH paid until %s (%d)!\n", - GNUNET_STRINGS_relative_time_to_string ( - GNUNET_TIME_absolute_get_remaining ( - paid_until), - GNUNET_YES), - qs); - } - else - { - paid_until - = GNUNET_TIME_relative_to_absolute ( - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, - ANASTASIS_MAX_YEARS_STORAGE)); } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "TRUTH paid until %s (%d)!\n", + GNUNET_TIME_relative2s ( + GNUNET_TIME_absolute_get_remaining ( + paid_until.abs_time), + GNUNET_YES), + qs); + } + else + { + paid_until + = GNUNET_TIME_relative_to_timestamp ( + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, + ANASTASIS_MAX_YEARS_STORAGE)); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Storing truth until %s!\n", - GNUNET_STRINGS_absolute_time_to_string (paid_until)); + "Storing truth %s until %s!\n", + TALER_B2S (truth_uuid), + GNUNET_TIME_timestamp2s (paid_until)); qs = db->store_truth (db->cls, truth_uuid, - &keyshare_data, + &key_share_data, (NULL == truth_mime) ? "" : truth_mime, encrypted_truth, encrypted_truth_size, type, - GNUNET_TIME_absolute_get_remaining (paid_until)); + GNUNET_TIME_absolute_get_remaining ( + paid_until.abs_time)); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -813,8 +740,8 @@ AH_handler_truth_post ( ok = ( (xtruth_size == encrypted_truth_size) && (0 == strcmp (xmethod, type)) && - (0 == strcmp (truth_mime, - xtruth_mime)) && + (0 == strcmp (((NULL == truth_mime) ? "" : truth_mime), + ((NULL == xtruth_mime) ? "" : xtruth_mime))) && (0 == memcmp (xtruth, encrypted_truth, xtruth_size)) ); diff --git a/src/backend/anastasis-httpd_truth.h b/src/backend/anastasis-httpd_truth.h index 87e570b..a436394 100644 --- a/src/backend/anastasis-httpd_truth.h +++ b/src/backend/anastasis-httpd_truth.h @@ -1,6 +1,6 @@ /* This file is part of Anastasis - Copyright (C) 2014, 2015, 2016, 2021 Anastasis SARL + Copyright (C) 2020-2022 Anastasis SARL Anastasis 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 @@ -24,13 +24,18 @@ #define ANASTASIS_HTTPD_TRUTH_H #include <microhttpd.h> - /** - * Prepare all active GET truth requests for system shutdown. + * Prepare all active POST truth solve requests for system shutdown. */ void -AH_truth_shutdown (void); +AH_truth_solve_shutdown (void); + +/** + * Prepare all active POST truth challenge requests for system shutdown. + */ +void +AH_truth_challenge_shutdown (void); /** * Prepare all active POST truth requests for system shutdown. @@ -40,36 +45,60 @@ AH_truth_upload_shutdown (void); /** - * Handle a GET to /truth/$UUID + * Handle a POST to /truth/$UUID. * - * @param connection the MHD connection to handle + * @param[in,out] connection the MHD connection to handle + * @param[in,out] hc connection context * @param truth_uuid the truth UUID - * @param hc connection context + * @param truth_data truth data + * @param truth_data_size number of bytes (left) in @a truth_data * @return MHD result code */ MHD_RESULT -AH_handler_truth_get ( +AH_handler_truth_post ( struct MHD_Connection *connection, + struct TM_HandlerContext *hc, const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, - struct TM_HandlerContext *hc); + const char *truth_data, + size_t *truth_data_size); /** - * Handle a POST to /truth/$UUID. + * Handle a POST to /truth/$UUID/solve. * - * @param connection the MHD connection to handle - * @param hc connection context + * @param[in,out] connection the MHD connection to handle + * @param[in,out] hc connection context * @param truth_uuid the truth UUID * @param truth_data truth data * @param truth_data_size number of bytes (left) in @a truth_data * @return MHD result code */ MHD_RESULT -AH_handler_truth_post ( +AH_handler_truth_solve ( struct MHD_Connection *connection, struct TM_HandlerContext *hc, const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, - const char *truth_data, - size_t *truth_data_size); + const char *upload_data, + size_t *upload_data_size); + + +/** + * Handle a POST to /truth/$UUID/challenge. + * + * @param[in,out] connection the MHD connection to handle + * @param[in,out] hc connection context + * @param truth_uuid the truth UUID + * @param truth_data truth data + * @param truth_data_size number of bytes (left) in @a truth_data + * @return MHD result code + */ +MHD_RESULT +AH_handler_truth_challenge ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *upload_data, + size_t *upload_data_size); + #endif diff --git a/src/backend/anastasis.conf b/src/backend/anastasis.conf index 8d2f0f6..4721760 100644 --- a/src/backend/anastasis.conf +++ b/src/backend/anastasis.conf @@ -49,18 +49,18 @@ UPLOAD_LIMIT_MB = 16 FULFILLMENT_URL = taler://fulfillment-success # Server salt 16 Byte -# SERVER_SALT = gUfO1KGOKYIFlFQg +# PROVIDER_SALT = gUfO1KGOKYIFlFQg # Directory with our terms of service. -TERMS_DIR = $DATADIR/tos/ +TERMS_DIR = ${DATADIR}tos/ # Etag / filename for the terms of service. TERMS_ETAG = 0 # Directory with our privacy policy. -PRIVACY_DIR = $DATADIR/pp/ +PRIVACY_DIR = ${DATADIR}pp/ # Etag / filename for the privacy policy. PRIVACY_ETAG = 0 |