/* This file is part of Anastasis Copyright (C) 2020, 2021, 2022 Anastasis SARL Anastasis is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Anastasis; see the file COPYING.GPL. If not, see */ /** * @file restclient/anastasis_api_truth_challenge.c * @brief Implementation of the POST /truth/$TID/challenge request on the client-side * @author Christian Grothoff * @author Dennis Neufeld * @author Dominik Meister */ #include "platform.h" #include #include #include /* just for HTTP status codes */ #include "anastasis_service.h" #include "anastasis_api_curl_defaults.h" #include #include #include /** * @brief A Contract Operation Handle */ struct ANASTASIS_TruthChallengeOperation { /** * The url for this request. */ char *url; /** * Handle for the request. */ struct GNUNET_CURL_Job *job; /** * Function to call with the result. */ ANASTASIS_TruthChallengeCallback cb; /** * Closure for @a cb. */ void *cb_cls; /** * Context for #TEH_curl_easy_post(). Keeps the data that must * persist for Curl to make the upload. */ struct TALER_CURL_PostContext ctx; /** * Payment URI we received from the service, or NULL. */ char *pay_uri; /** * Content type of the body. */ char *content_type; }; void ANASTASIS_truth_challenge_cancel ( struct ANASTASIS_TruthChallengeOperation *tco) { if (NULL != tco->job) { GNUNET_CURL_job_cancel (tco->job); tco->job = NULL; } GNUNET_free (tco->pay_uri); GNUNET_free (tco->url); GNUNET_free (tco->content_type); TALER_curl_easy_post_finished (&tco->ctx); GNUNET_free (tco); } /** * Process POST /truth/$TID/challenge response * * @param cls our `struct ANASTASIS_TruthChallengeOperation *` * @param response_code the HTTP status * @param response parsed JSON result, NULL one rrro */ static void handle_truth_challenge_finished (void *cls, long response_code, const void *response) { struct ANASTASIS_TruthChallengeOperation *tco = cls; const json_t *j = response; struct ANASTASIS_TruthChallengeDetails tcd = { .http_status = response_code, .response = j }; tco->job = NULL; switch (response_code) { case 0: /* Hard error */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Backend didn't even return from POST /truth/$TID/challenge\n"); tcd.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; case MHD_HTTP_OK: { const char *ct; const char *tan_hint = NULL; const char *filename = NULL; const json_t *wire_details = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ( "challenge_type", &ct), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("tan_address_hint", &tan_hint), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("filename", &filename), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_object_const ("wire_details", &wire_details), NULL), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (j, spec, NULL, NULL)) { GNUNET_break_op (0); tcd.http_status = 0; tcd.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } if (0 == strcmp (ct, "TAN_SENT")) { tcd.details.success.cs = ANASTASIS_CS_TAN_SENT; tcd.details.success.details.tan_address_hint = tan_hint; break; } if (0 == strcmp (ct, "TAN_ALREADY_SENT")) { tcd.details.success.cs = ANASTASIS_CS_TAN_ALREADY_SENT; break; } if ( (0 == strcmp (ct, "FILE_WRITTEN")) && (NULL != filename) ) { tcd.details.success.cs = ANASTASIS_CS_FILE_WRITTEN; tcd.details.success.details.challenge_filename = filename; break; } if ( (0 == strcmp (ct, "IBAN_WIRE")) && (NULL != wire_details) ) { struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_string ( "credit_iban", &tcd.details.success.details.wire_funds.target_iban), GNUNET_JSON_spec_uint64 ( "answer_code", &tcd.details.success.details.wire_funds.answer_code), GNUNET_JSON_spec_string ( "business_name", &tcd.details.success.details.wire_funds.target_business_name), GNUNET_JSON_spec_string ( "wire_transfer_subject", &tcd.details.success.details.wire_funds.wire_transfer_subject), TALER_JSON_spec_amount_any ("challenge_amount", &tcd.details.success.details.wire_funds. amount), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (wire_details, ispec, NULL, NULL)) { GNUNET_break_op (0); tcd.http_status = 0; tcd.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } tcd.details.success.cs = ANASTASIS_CS_WIRE_FUNDS; tco->cb (tco->cb_cls, &tcd); ANASTASIS_truth_challenge_cancel (tco); return; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected challenge type `%s'\n", ct); json_dumpf (j, stderr, JSON_INDENT (2)); tcd.http_status = 0; tcd.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } case MHD_HTTP_BAD_REQUEST: /* This should never happen, either us or the anastasis server is buggy (or API version conflict); just pass JSON reply to the application */ GNUNET_break (0); tcd.ec = TALER_JSON_get_error_code (j); break; case MHD_HTTP_PAYMENT_REQUIRED: { struct TALER_MERCHANT_PayUriData pd; if ( (NULL == tco->pay_uri) || (GNUNET_OK != TALER_MERCHANT_parse_pay_uri (tco->pay_uri, &pd)) ) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse `%s'\n", tco->pay_uri); tcd.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } if (GNUNET_OK != GNUNET_STRINGS_string_to_data ( pd.order_id, strlen (pd.order_id), &tcd.details.payment_required.ps, sizeof (tcd.details.payment_required.ps))) { GNUNET_break (0); tcd.ec = TALER_EC_GENERIC_REPLY_MALFORMED; TALER_MERCHANT_parse_pay_uri_free (&pd); break; } tcd.details.payment_required.pd = &pd; tcd.details.payment_required.payment_request = tco->pay_uri; tco->cb (tco->cb_cls, &tcd); TALER_MERCHANT_parse_pay_uri_free (&pd); ANASTASIS_truth_challenge_cancel (tco); return; } break; case MHD_HTTP_FORBIDDEN: /* Nothing really to verify, authentication required/failed */ tcd.ec = TALER_JSON_get_error_code (j); break; case MHD_HTTP_NOT_FOUND: /* Nothing really to verify */ tcd.ec = TALER_JSON_get_error_code (j); break; case MHD_HTTP_INTERNAL_SERVER_ERROR: tcd.ec = TALER_JSON_get_error_code (j); break; case MHD_HTTP_BAD_GATEWAY: tcd.ec = TALER_JSON_get_error_code (j); break; default: /* unexpected response code */ tcd.ec = TALER_JSON_get_error_code (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d to POST /truth/$TID/challenge\n", (unsigned int) response_code, (int) tcd.ec); GNUNET_break (0); break; } tco->cb (tco->cb_cls, &tcd); ANASTASIS_truth_challenge_cancel (tco); } /** * Patch value in @a val, replacing new line with '\0'. * * @param[in,out] val 0-terminated string to replace '\\n' and '\\r' with '\\0' in. */ static void patch_value (char *val) { size_t len; /* found location URI we care about! */ len = strlen (val); while ( (len > 0) && ( ('\n' == val[len - 1]) || ('\r' == val[len - 1]) ) ) { len--; val[len] = '\0'; } } /** * Handle HTTP header received by curl. * * @param buffer one line of HTTP header data * @param size size of an item * @param nitems number of items passed * @param userdata our `struct ANASTASIS_StorePolicyOperation *` * @return `size * nitems` */ static size_t handle_header (char *buffer, size_t size, size_t nitems, void *userdata) { struct ANASTASIS_TruthChallengeOperation *tco = userdata; size_t total = size * nitems; char *ndup; const char *hdr_type; char *hdr_val; char *sp; ndup = GNUNET_strndup (buffer, total); hdr_type = strtok_r (ndup, ":", &sp); if (NULL == hdr_type) { GNUNET_free (ndup); return total; } hdr_val = strtok_r (NULL, "", &sp); if (NULL == hdr_val) { GNUNET_free (ndup); return total; } if (' ' == *hdr_val) hdr_val++; if (0 == strcasecmp (hdr_type, ANASTASIS_HTTP_HEADER_TALER)) { /* found payment URI we care about! */ GNUNET_free (tco->pay_uri); tco->pay_uri = GNUNET_strdup (hdr_val); patch_value (tco->pay_uri); } if (0 == strcasecmp (hdr_type, MHD_HTTP_HEADER_CONTENT_TYPE)) { /* found location URI we care about! */ GNUNET_free (tco->content_type); tco->content_type = GNUNET_strdup (hdr_val); patch_value (tco->content_type); } GNUNET_free (ndup); return total; } struct ANASTASIS_TruthChallengeOperation * ANASTASIS_truth_challenge ( struct GNUNET_CURL_Context *ctx, const char *backend_url, const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, const struct ANASTASIS_CRYPTO_TruthKeyP *truth_key, const struct ANASTASIS_PaymentSecretP *payment_secret, ANASTASIS_TruthChallengeCallback cb, void *cb_cls) { struct ANASTASIS_TruthChallengeOperation *tco; CURL *eh; json_t *body; body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("truth_decryption_key", truth_key), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_auto ("payment_secret", payment_secret))); GNUNET_assert (NULL != body); tco = GNUNET_new (struct ANASTASIS_TruthChallengeOperation); tco->cb = cb; tco->cb_cls = cb_cls; { char *path; char *uuid_str; uuid_str = GNUNET_STRINGS_data_to_string_alloc (truth_uuid, sizeof (*truth_uuid)); GNUNET_asprintf (&path, "truth/%s/challenge", uuid_str); GNUNET_free (uuid_str); tco->url = TALER_url_join (backend_url, path, NULL); GNUNET_free (path); } eh = ANASTASIS_curl_easy_get_ (tco->url); if ( (NULL == eh) || (GNUNET_OK != TALER_curl_easy_post (&tco->ctx, eh, body)) ) { GNUNET_break (0); if (NULL != eh) curl_easy_cleanup (eh); json_decref (body); GNUNET_free (tco->url); GNUNET_free (tco); return NULL; } json_decref (body); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &handle_header)); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_HEADERDATA, tco)); tco->job = GNUNET_CURL_job_add2 (ctx, eh, tco->ctx.headers, &handle_truth_challenge_finished, tco); return tco; } /* end of anastasis_api_truth_challenge.c */