/*
This file is part of Anastasis
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
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
*/
/**
* @file anastasis-httpd_truth_upload.c
* @brief functions to handle incoming POST request on /truth
* @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
#include
#include
#include
#include
#include "anastasis_authorization_lib.h"
/**
* Information we track per truth upload.
*/
struct TruthUploadContext
{
/**
* UUID of the truth object we are processing.
*/
struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
/**
* Kept in DLL for shutdown handling while suspended.
*/
struct TruthUploadContext *next;
/**
* Kept in DLL for shutdown handling while suspended.
*/
struct TruthUploadContext *prev;
/**
* Used while we are awaiting proposal creation.
*/
struct TALER_MERCHANT_PostOrdersHandle *po;
/**
* Used while we are waiting payment.
*/
struct TALER_MERCHANT_OrderMerchantGetHandle *cpo;
/**
* Post parser context.
*/
void *post_ctx;
/**
* Handle to the client request.
*/
struct MHD_Connection *connection;
/**
* Incoming JSON, NULL if not yet available.
*/
json_t *json;
/**
* HTTP response code to use on resume, if non-NULL.
*/
struct MHD_Response *resp;
/**
* When should this request time out?
*/
struct GNUNET_TIME_Absolute timeout;
/**
* Fee that is to be paid for this upload.
*/
struct TALER_Amount upload_fee;
/**
* HTTP response code to use on resume, if resp is set.
*/
unsigned int response_code;
/**
* For how many years must the customer still pay?
*/
unsigned int years_to_pay;
};
/**
* Head of linked list over all truth upload processes
*/
static struct TruthUploadContext *tuc_head;
/**
* Tail of linked list over all truth upload processes
*/
static struct TruthUploadContext *tuc_tail;
void
AH_truth_upload_shutdown (void)
{
struct TruthUploadContext *tuc;
while (NULL != (tuc = tuc_head))
{
GNUNET_CONTAINER_DLL_remove (tuc_head,
tuc_tail,
tuc);
if (NULL != tuc->cpo)
{
TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo);
tuc->cpo = NULL;
}
if (NULL != tuc->po)
{
TALER_MERCHANT_orders_post_cancel (tuc->po);
tuc->po = NULL;
}
MHD_resume_connection (tuc->connection);
}
}
/**
* Function called to clean up a `struct TruthUploadContext`.
*
* @param hc general handler context
*/
static void
cleanup_truth_post (struct TM_HandlerContext *hc)
{
struct TruthUploadContext *tuc = hc->ctx;
TALER_MHD_parse_post_cleanup_callback (tuc->post_ctx);
if (NULL != tuc->po)
TALER_MERCHANT_orders_post_cancel (tuc->po);
if (NULL != tuc->cpo)
TALER_MERCHANT_merchant_order_get_cancel (tuc->cpo);
if (NULL != tuc->resp)
MHD_destroy_response (tuc->resp);
if (NULL != tuc->json)
json_decref (tuc->json);
GNUNET_free (tuc);
}
/**
* Transmit a payment request for @a tuc.
*
* @param tuc upload context to generate payment request for
*/
static void
make_payment_request (struct TruthUploadContext *tuc)
{
struct MHD_Response *resp;
/* request payment via Taler */
resp = MHD_create_response_from_buffer (0,
NULL,
MHD_RESPMEM_PERSISTENT);
GNUNET_assert (NULL != resp);
TALER_MHD_add_global_headers (resp);
{
char *hdr;
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));
{
char *order_id;
order_id = GNUNET_STRINGS_data_to_string_alloc (
&tuc->truth_uuid,
sizeof (tuc->truth_uuid));
GNUNET_asprintf (&hdr,
"%spay/%s%s/",
pfx,
hn,
order_id);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Returning %u %s\n",
MHD_HTTP_PAYMENT_REQUIRED,
order_id);
GNUNET_free (order_id);
}
GNUNET_break (MHD_YES ==
MHD_add_response_header (resp,
ANASTASIS_HTTP_HEADER_TALER,
hdr));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"TRUTH payment request made: %s\n",
hdr);
GNUNET_free (hdr);
}
tuc->resp = resp;
tuc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
}
/**
* Callbacks of this type are used to serve the result of submitting a
* POST /private/orders request to a merchant.
*
* @param cls our `struct TruthUploadContext`
* @param por response details
*/
static void
proposal_cb (void *cls,
const struct TALER_MERCHANT_PostOrdersReply *por)
{
struct TruthUploadContext *tuc = cls;
tuc->po = NULL;
GNUNET_CONTAINER_DLL_remove (tuc_head,
tuc_tail,
tuc);
MHD_resume_connection (tuc->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);
tuc->resp = TALER_MHD_MAKE_JSON_PACK (
GNUNET_JSON_pack_uint64 ("code",
TALER_EC_ANASTASIS_GENERIC_ORDER_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_incref ("backend-reply",
(json_t *) por->hr.reply)));
tuc->response_code = MHD_HTTP_BAD_GATEWAY;
return;
}
make_payment_request (tuc);
}
/**
* Callback to process a GET /check-payment request
*
* @param cls our `struct PolicyUploadContext`
* @param osr order status
*/
static void
check_payment_cb (void *cls,
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,
"Checking backend order status returned %u\n",
hr->http_status);
switch (hr->http_status)
{
case 0:
/* Likely timeout, complain! */
tuc->response_code = MHD_HTTP_GATEWAY_TIMEOUT;
tuc->resp = TALER_MHD_make_error (
TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT,
NULL);
break;
case MHD_HTTP_OK:
switch (osr->details.ok.status)
{
case TALER_MERCHANT_OSC_PAID:
{
enum GNUNET_DB_QueryStatus qs;
unsigned int years;
struct GNUNET_TIME_Relative paid_until;
const json_t *contract;
struct TALER_Amount amount;
struct GNUNET_JSON_Specification cspec[] = {
TALER_JSON_spec_amount_any ("amount",
&amount),
GNUNET_JSON_spec_end ()
};
contract = osr->details.ok.details.paid.contract_terms;
if (GNUNET_OK !=
GNUNET_JSON_parse (contract,
cspec,
NULL, NULL))
{
GNUNET_break (0);
tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
tuc->resp = TALER_MHD_make_error (
TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
"contract terms in database are malformed");
break;
}
years = TALER_amount_divide2 (&amount,
&AH_truth_upload_fee);
paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
years);
/* add 1 week grace period, otherwise if a user
wants to pay for 1 year, the first seconds
would have passed between making the payment
and our subsequent check if +1 year was
paid... So we actually say 1 year = 52 weeks
on the server, while the client calculates
with 365 days. */
paid_until = GNUNET_TIME_relative_add (paid_until,
GNUNET_TIME_UNIT_WEEKS);
qs = db->record_truth_upload_payment (
db->cls,
&tuc->truth_uuid,
&osr->details.ok.details.paid.deposit_total,
paid_until);
if (qs <= 0)
{
GNUNET_break (0);
tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
tuc->resp = TALER_MHD_make_error (
TALER_EC_GENERIC_DB_STORE_FAILED,
"record_truth_upload_payment");
break;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Payment confirmed, resuming upload\n");
break;
case TALER_MERCHANT_OSC_UNPAID:
case TALER_MERCHANT_OSC_CLAIMED:
make_payment_request (tuc);
break;
}
break;
case MHD_HTTP_UNAUTHORIZED:
/* Configuration issue, complain! */
tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
tuc->resp = TALER_MHD_MAKE_JSON_PACK (
GNUNET_JSON_pack_uint64 ("code",
TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED),
GNUNET_JSON_pack_string ("hint",
TALER_ErrorCode_get_hint (
TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED)),
GNUNET_JSON_pack_uint64 ("backend-ec",
hr->ec),
GNUNET_JSON_pack_uint64 ("backend-http-status",
hr->http_status),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("backend-reply",
(json_t *) hr->reply)));
GNUNET_assert (NULL != tuc->resp);
break;
case MHD_HTTP_NOT_FOUND:
/* Setup fresh order */
{
static const char *no_uuids[1] = { NULL };
char *order_id;
json_t *order;
order_id = GNUNET_STRINGS_data_to_string_alloc (
&tuc->truth_uuid,
sizeof(tuc->truth_uuid));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"%u, setting up fresh order %s\n",
MHD_HTTP_NOT_FOUND,
order_id);
order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s}",
"amount",
TALER_JSON_from_amount (&tuc->upload_fee),
"summary",
"Anastasis challenge storage fee",
"products",
"description", "challenge storage fee",
"quantity", (json_int_t) tuc->years_to_pay,
"unit", "years",
"order_id",
order_id);
GNUNET_free (order_id);
tuc->po = TALER_MERCHANT_orders_post2 (AH_ctx,
AH_backend_url,
order,
GNUNET_TIME_UNIT_ZERO,
NULL, /* no payment target */
0,
NULL, /* no inventory products */
0,
no_uuids, /* no uuids */
false, /* do NOT require claim token */
&proposal_cb,
tuc);
AH_trigger_curl ();
json_decref (order);
return;
}
default:
/* Unexpected backend response */
tuc->response_code = MHD_HTTP_BAD_GATEWAY;
tuc->resp = TALER_MHD_MAKE_JSON_PACK (
GNUNET_JSON_pack_uint64 ("code",
TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR),
GNUNET_JSON_pack_string ("hint",
TALER_ErrorCode_get_hint (
TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR)),
GNUNET_JSON_pack_uint64 ("backend-ec",
(json_int_t) hr->ec),
GNUNET_JSON_pack_uint64 ("backend-http-status",
(json_int_t) hr->http_status),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("backend-reply",
(json_t *) hr->reply)));
break;
}
GNUNET_CONTAINER_DLL_remove (tuc_head,
tuc_tail,
tuc);
MHD_resume_connection (tuc->connection);
AH_trigger_daemon (NULL);
}
/**
* Helper function used to ask our backend to begin processing a
* payment for the truth upload. May perform asynchronous operations
* by suspending the connection if required.
*
* @param tuc context to begin payment for.
* @return MHD status code
*/
static MHD_RESULT
begin_payment (struct TruthUploadContext *tuc)
{
char *order_id;
struct GNUNET_TIME_Relative timeout;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Checking backend order status...\n");
timeout = GNUNET_TIME_absolute_get_remaining (tuc->timeout);
order_id = GNUNET_STRINGS_data_to_string_alloc (
&tuc->truth_uuid,
sizeof (tuc->truth_uuid));
tuc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx,
AH_backend_url,
order_id,
NULL /* our payments are NOT session-bound */,
timeout,
&check_payment_cb,
tuc);
GNUNET_free (order_id);
if (NULL == tuc->cpo)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (tuc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED,
"Could not check order status");
}
GNUNET_CONTAINER_DLL_insert (tuc_head,
tuc_tail,
tuc);
MHD_suspend_connection (tuc->connection);
return MHD_YES;
}
MHD_RESULT
AH_handler_truth_post (
struct MHD_Connection *connection,
struct TM_HandlerContext *hc,
const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
const char *truth_data,
size_t *truth_data_size)
{
struct TruthUploadContext *tuc = hc->ctx;
MHD_RESULT ret;
int res;
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_Timestamp paid_until
= GNUNET_TIME_UNIT_ZERO_TS;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("key_share_data",
&key_share_data),
GNUNET_JSON_spec_string ("type",
&type),
GNUNET_JSON_spec_varsize ("encrypted_truth",
&encrypted_truth,
&encrypted_truth_size),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("truth_mime",
&truth_mime),
NULL),
GNUNET_JSON_spec_uint32 ("storage_duration_years",
&storage_years),
GNUNET_JSON_spec_end ()
};
if (NULL == tuc)
{
tuc = GNUNET_new (struct TruthUploadContext);
tuc->connection = connection;
tuc->truth_uuid = *truth_uuid;
hc->ctx = tuc;
hc->cc = &cleanup_truth_post;
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)
{
/* We generated a response asynchronously, queue that */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Returning asynchronously generated response with HTTP status %u\n",
tuc->response_code);
ret = MHD_queue_response (connection,
tuc->response_code,
tuc->resp);
GNUNET_break (MHD_YES == ret);
MHD_destroy_response (tuc->resp);
tuc->resp = NULL;
return ret;
}
if (NULL == tuc->json)
{
res = TALER_MHD_parse_post_json (connection,
&tuc->post_ctx,
truth_data,
truth_data_size,
&tuc->json);
if (GNUNET_SYSERR == res)
{
GNUNET_break (0);
return MHD_NO;
}
if ( (GNUNET_NO == res) ||
(NULL == tuc->json) )
return MHD_YES;
}
res = TALER_MHD_parse_json_data (connection,
tuc->json,
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 */
}
/* check method is supported */
if ( (0 != strcmp ("question",
type)) &&
(NULL ==
ANASTASIS_authorization_plugin_load (type,
db,
AH_cfg)) )
{
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED,
type);
}
if (storage_years > ANASTASIS_MAX_YEARS_STORAGE)
{
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 == storage_years)
storage_years = 1;
if (! TALER_amount_is_zero (&AH_truth_upload_fee))
{
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_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_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"storage_duration_years");
}
if (! TALER_amount_is_zero (&tuc->upload_fee))
{
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_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 %s until %s!\n",
TALER_B2S (truth_uuid),
GNUNET_TIME_timestamp2s (paid_until));
qs = db->store_truth (db->cls,
truth_uuid,
&key_share_data,
(NULL == truth_mime)
? ""
: truth_mime,
encrypted_truth,
encrypted_truth_size,
type,
GNUNET_TIME_absolute_get_remaining (
paid_until.abs_time));
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
case GNUNET_DB_STATUS_SOFT_ERROR:
GNUNET_break (0);
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
"store_truth");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
{
void *xtruth;
size_t xtruth_size;
char *xtruth_mime;
char *xmethod;
bool ok = false;
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
db->get_escrow_challenge (db->cls,
truth_uuid,
&xtruth,
&xtruth_size,
&xtruth_mime,
&xmethod))
{
ok = ( (xtruth_size == encrypted_truth_size) &&
(0 == strcmp (xmethod,
type)) &&
(0 == strcmp (((NULL == truth_mime) ? "" : truth_mime),
((NULL == xtruth_mime) ? "" : xtruth_mime))) &&
(0 == memcmp (xtruth,
encrypted_truth,
xtruth_size)) );
GNUNET_free (encrypted_truth);
GNUNET_free (xtruth_mime);
GNUNET_free (xmethod);
}
if (! ok)
{
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_CONFLICT,
TALER_EC_ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS,
NULL);
}
/* idempotency detected, intentional fall through! */
}
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
{
struct MHD_Response *resp;
GNUNET_JSON_parse_free (spec);
resp = MHD_create_response_from_buffer (0,
NULL,
MHD_RESPMEM_PERSISTENT);
TALER_MHD_add_global_headers (resp);
ret = MHD_queue_response (connection,
MHD_HTTP_NO_CONTENT,
resp);
MHD_destroy_response (resp);
GNUNET_break (MHD_YES == ret);
return ret;
}
}
GNUNET_JSON_parse_free (spec);
GNUNET_break (0);
return MHD_NO;
}