summaryrefslogtreecommitdiff
path: root/src/backend/anastasis-httpd_policy_upload.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/anastasis-httpd_policy_upload.c')
-rw-r--r--src/backend/anastasis-httpd_policy_upload.c1211
1 files changed, 1211 insertions, 0 deletions
diff --git a/src/backend/anastasis-httpd_policy_upload.c b/src/backend/anastasis-httpd_policy_upload.c
new file mode 100644
index 0000000..b8bd5ed
--- /dev/null
+++ b/src/backend/anastasis-httpd_policy_upload.c
@@ -0,0 +1,1211 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2021 Anastasis SARL
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file anastasis-httpd_policy.c
+ * @brief functions to handle incoming requests on /policy/
+ * @author Dennis Neufeld
+ * @author Dominik Meister
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "anastasis-httpd.h"
+#include "anastasis-httpd_policy.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>
+
+/**
+ * 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)
+
+
+/**
+ * Context for an upload operation.
+ */
+struct PolicyUploadContext
+{
+
+ /**
+ * Signature of the account holder.
+ */
+ struct ANASTASIS_AccountSignatureP account_sig;
+
+ /**
+ * Public key of the account holder.
+ */
+ struct ANASTASIS_CRYPTO_AccountPublicKeyP account;
+
+ /**
+ * Hash of the upload we are receiving right now (as promised
+ * by the client, to be verified!).
+ */
+ struct GNUNET_HashCode new_policy_upload_hash;
+
+ /**
+ * Hash context for the upload.
+ */
+ struct GNUNET_HashContext *hash_ctx;
+
+ /**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+ struct PolicyUploadContext *next;
+
+ /**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+ struct PolicyUploadContext *prev;
+
+ /**
+ * Used while suspended for resumption.
+ */
+ struct MHD_Connection *con;
+
+ /**
+ * Upload, with as many bytes as we have received so far.
+ */
+ char *upload;
+
+ /**
+ * 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;
+
+ /**
+ * Order under which the client promised payment, or NULL.
+ */
+ const char *order_id;
+
+ /**
+ * Payment Identifier
+ */
+ struct ANASTASIS_PaymentSecretP payment_identifier;
+
+ /**
+ * Timestamp of the order in @e payment_identifier. Used to
+ * select the most recent unpaid offer.
+ */
+ struct GNUNET_TIME_Absolute existing_pi_timestamp;
+
+ /**
+ * When does the operation timeout?
+ */
+ struct GNUNET_TIME_Absolute timeout;
+
+ /**
+ * How long must the account be valid? Determines whether we should
+ * trigger payment, and if so how much.
+ */
+ struct GNUNET_TIME_Absolute end_date;
+
+ /**
+ * How long is the account already valid?
+ * Determines how much the user needs to pay.
+ */
+ struct GNUNET_TIME_Absolute paid_until;
+
+ /**
+ * Expected total upload size.
+ */
+ size_t upload_size;
+
+ /**
+ * Current offset for the upload.
+ */
+ size_t upload_off;
+
+ /**
+ * HTTP response code to use on resume, if resp is set.
+ */
+ unsigned int response_code;
+
+ /**
+ * For how many years does the client still have
+ * to pay?
+ */
+ unsigned int years_to_pay;
+
+ /**
+ * true if client provided a payment secret / order ID?
+ */
+ bool payment_identifier_provided;
+
+};
+
+
+/**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+static struct PolicyUploadContext *puc_head;
+
+/**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+static struct PolicyUploadContext *puc_tail;
+
+
+/**
+ * Service is shutting down, resume all MHD connections NOW.
+ */
+void
+AH_resume_all_bc ()
+{
+ struct PolicyUploadContext *puc;
+
+ while (NULL != (puc = puc_head))
+ {
+ GNUNET_CONTAINER_DLL_remove (puc_head,
+ puc_tail,
+ puc);
+ if (NULL != puc->po)
+ {
+ TALER_MERCHANT_orders_post_cancel (puc->po);
+ puc->po = NULL;
+ }
+ if (NULL != puc->cpo)
+ {
+ TALER_MERCHANT_merchant_order_get_cancel (puc->cpo);
+ puc->cpo = NULL;
+ }
+ MHD_resume_connection (puc->con);
+ }
+}
+
+
+/**
+ * Function called to clean up a backup context.
+ *
+ * @param hc a `struct PolicyUploadContext`
+ */
+static void
+cleanup_ctx (struct TM_HandlerContext *hc)
+{
+ struct PolicyUploadContext *puc = hc->ctx;
+
+ if (NULL != puc->po)
+ TALER_MERCHANT_orders_post_cancel (puc->po);
+ if (NULL != puc->cpo)
+ TALER_MERCHANT_merchant_order_get_cancel (puc->cpo);
+ if (NULL != puc->hash_ctx)
+ GNUNET_CRYPTO_hash_context_abort (puc->hash_ctx);
+ if (NULL != puc->resp)
+ MHD_destroy_response (puc->resp);
+ GNUNET_free (puc->upload);
+ GNUNET_free (puc);
+}
+
+
+/**
+ * Transmit a payment request for @a order_id on @a connection
+ *
+ * @param connection MHD connection
+ * @param order_id our backend's order ID
+ * @return #GNUNET_OK on success
+ */
+static int
+make_payment_request (struct PolicyUploadContext *puc)
+{
+ struct MHD_Response *resp;
+
+ /* request payment via Taler */
+ resp = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ if (NULL == resp)
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ TALER_MHD_add_global_headers (resp);
+ {
+ char *hdr;
+ char *pfx;
+ 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
+ {
+ GNUNET_break (0);
+ MHD_destroy_response (resp);
+ return GNUNET_SYSERR;
+ }
+ if (0 == strlen (hn))
+ {
+ GNUNET_break (0);
+ MHD_destroy_response (resp);
+ return GNUNET_SYSERR;
+ }
+ {
+ char *order_id;
+
+ order_id = GNUNET_STRINGS_data_to_string_alloc (
+ &puc->payment_identifier,
+ sizeof (puc->payment_identifier));
+ GNUNET_asprintf (&hdr,
+ "%spay/%s%s/",
+ pfx,
+ hn,
+ order_id);
+ GNUNET_free (order_id);
+ }
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (resp,
+ ANASTASIS_HTTP_HEADER_TALER,
+ hdr));
+ GNUNET_free (hdr);
+ }
+ puc->resp = resp;
+ puc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
+ return GNUNET_OK;
+}
+
+
+/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * POST /private/orders request to a merchant.
+ *
+ * @param cls our `struct PolicyUploadContext`
+ * @param por response details
+ */
+static void
+proposal_cb (void *cls,
+ const struct TALER_MERCHANT_PostOrdersReply *por)
+{
+ struct PolicyUploadContext *puc = cls;
+ enum GNUNET_DB_QueryStatus qs;
+
+ puc->po = NULL;
+ GNUNET_CONTAINER_DLL_remove (puc_head,
+ puc_tail,
+ puc);
+ MHD_resume_connection (puc->con);
+ 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);
+ puc->resp = TALER_MHD_make_json_pack (
+ "{s:I, s:s, s:I, s:I, s:O?}",
+ "code",
+ (json_int_t) TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR,
+ "hint",
+ "Failed to setup order with merchant backend",
+ "backend-ec",
+ (json_int_t) por->hr.ec,
+ "backend-http-status",
+ (json_int_t) por->hr.http_status,
+ "backend-reply",
+ por->hr.reply);
+ GNUNET_assert (NULL != puc->resp);
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Storing payment request for order `%s'\n",
+ por->details.ok.order_id);
+
+ qs = db->record_recdoc_payment (db->cls,
+ &puc->account,
+ (uint32_t) AH_post_counter,
+ &puc->payment_identifier,
+ &AH_annual_fee);
+ if (0 >= qs)
+ {
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "record recdoc payment");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return;
+ }
+ if (GNUNET_OK !=
+ make_payment_request (puc))
+ {
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "failed to initiate payment");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+}
+
+
+/**
+ * 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;
+
+ /* refunds are not supported, verify */
+ puc->cpo = NULL;
+ GNUNET_CONTAINER_DLL_remove (puc_head,
+ puc_tail,
+ puc);
+ MHD_resume_connection (puc->con);
+ AH_trigger_daemon (NULL);
+ switch (hr->http_status)
+ {
+ case MHD_HTTP_OK:
+ GNUNET_assert (NULL != osr);
+ break; /* processed below */
+ case MHD_HTTP_UNAUTHORIZED:
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED,
+ NULL);
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return;
+ default:
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR,
+ "failed to initiate payment");
+ puc->response_code = MHD_HTTP_BAD_GATEWAY;
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment status checked: %s\n",
+ osr->status ? "paid" : "unpaid");
+ switch (osr->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 ("amount",
+ AH_currency,
+ &amount),
+ GNUNET_JSON_spec_end ()
+ };
+
+ contract = osr->details.paid.contract_terms;
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (contract,
+ cspec,
+ NULL, NULL))
+ {
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ "no amount given");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return; /* continue as planned */
+ }
+ years = TALER_amount_divide2 (&amount,
+ &AH_annual_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->increment_lifetime (db->cls,
+ &puc->account,
+ &puc->payment_identifier,
+ paid_until,
+ &puc->paid_until);
+ if (0 <= qs)
+ return; /* continue as planned */
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "increment lifetime");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return; /* continue as planned */
+ }
+ case TALER_MERCHANT_OSC_UNPAID:
+ case TALER_MERCHANT_OSC_CLAIMED:
+ break;
+ }
+ if (0 != puc->existing_pi_timestamp.abs_value_us)
+ {
+ /* repeat payment request */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Repeating payment request\n");
+ if (GNUNET_OK !=
+ make_payment_request (puc))
+ {
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "failed to initiate payment");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Timeout waiting for payment\n");
+ puc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT,
+ "Timeout awaiting promised payment");
+ GNUNET_assert (NULL != puc->resp);
+ puc->response_code = MHD_HTTP_REQUEST_TIMEOUT;
+}
+
+
+/**
+ * Helper function used to ask our backend to await
+ * a payment for the user's account.
+ *
+ * @param puc context to begin payment for.
+ * @param timeout when to give up trying
+ */
+static void
+await_payment (struct PolicyUploadContext *puc)
+{
+ struct GNUNET_TIME_Relative timeout
+ = GNUNET_TIME_absolute_get_remaining (puc->timeout);
+
+ GNUNET_CONTAINER_DLL_insert (puc_head,
+ puc_tail,
+ puc);
+ MHD_suspend_connection (puc->con);
+ {
+ char *order_id;
+
+ order_id = GNUNET_STRINGS_data_to_string_alloc (
+ &puc->payment_identifier,
+ sizeof(struct ANASTASIS_PaymentSecretP));
+ puc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx,
+ AH_backend_url,
+ order_id,
+ NULL /* our payments are NOT session-bound */,
+ false,
+ timeout,
+ &check_payment_cb,
+ puc);
+ GNUNET_free (order_id);
+ }
+ AH_trigger_curl ();
+}
+
+
+/**
+ * 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 puc context to begin payment for.
+ * @return MHD status code
+ */
+static MHD_RESULT
+begin_payment (struct PolicyUploadContext *puc)
+{
+ json_t *order;
+
+ GNUNET_CONTAINER_DLL_insert (puc_head,
+ puc_tail,
+ puc);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Suspending connection while creating order at `%s'\n",
+ AH_backend_url);
+ {
+ char *order_id;
+ struct TALER_Amount upload_fee;
+
+ if (0 >
+ TALER_amount_multiply (&upload_fee,
+ &AH_annual_fee,
+ puc->years_to_pay))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "storage_duration_years");
+ }
+
+ order_id = GNUNET_STRINGS_data_to_string_alloc (
+ &puc->payment_identifier,
+ sizeof(struct ANASTASIS_PaymentSecretP));
+ 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",
+ "products",
+ "description", "policy storage fee",
+ "quantity", (json_int_t) puc->years_to_pay,
+ "unit", "years",
+ "order_id", order_id);
+ GNUNET_free (order_id);
+ }
+ MHD_suspend_connection (puc->con);
+ puc->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,
+ NULL, /* no uuids */
+ false, /* do NOT require claim token */
+ &proposal_cb,
+ puc);
+ AH_trigger_curl ();
+ json_decref (order);
+ return MHD_YES;
+}
+
+
+/**
+ * Prepare to receive a payment, possibly requesting it, or just waiting
+ * for it to be completed by the client.
+ *
+ * @param puc context to prepare payment for
+ * @return MHD status
+ */
+static MHD_RESULT
+prepare_payment (struct PolicyUploadContext *puc)
+{
+ if (! puc->payment_identifier_provided)
+ {
+ GNUNET_CRYPTO_random_block (
+ GNUNET_CRYPTO_QUALITY_NONCE,
+ &puc->payment_identifier,
+ sizeof (struct ANASTASIS_PaymentSecretP));
+ puc->payment_identifier_provided = true;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "No payment identifier, initiating payment\n");
+ return begin_payment (puc);
+ }
+ await_payment (puc);
+ return MHD_YES;
+}
+
+
+MHD_RESULT
+AH_handler_policy_post (
+ struct MHD_Connection *connection,
+ struct TM_HandlerContext *hc,
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
+ const char *recovery_data,
+ size_t *recovery_data_size)
+{
+ struct PolicyUploadContext *puc = hc->ctx;
+
+ if (NULL == puc)
+ {
+ /* first call, setup internals */
+ puc = GNUNET_new (struct PolicyUploadContext);
+ hc->ctx = puc;
+ hc->cc = &cleanup_ctx;
+ puc->con = connection;
+
+ {
+ 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),
+ &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);
+ }
+ }
+ puc->account = *account_pub;
+ /* now setup 'puc' */
+ {
+ const char *lens;
+ unsigned long len;
+
+ lens = MHD_lookup_connection_value (connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_CONTENT_LENGTH);
+ if ( (NULL == lens) ||
+ (1 != sscanf (lens,
+ "%lu",
+ &len)) )
+ {
+ 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");
+ }
+ puc->upload = GNUNET_malloc_large (len);
+ if (NULL == puc->upload)
+ {
+ GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
+ "malloc");
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_PAYLOAD_TOO_LARGE,
+ TALER_EC_ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH,
+ NULL);
+ }
+ 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");
+ }
+ }
+ {
+ /* Check if header contains an ETAG */
+ const char *etag;
+
+ etag = MHD_lookup_connection_value (connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_IF_NONE_MATCH);
+ if ( (NULL == etag) ||
+ (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (etag,
+ strlen (etag),
+ &puc->new_policy_upload_hash,
+ sizeof (puc->new_policy_upload_hash))) )
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_ANASTASIS_POLICY_BAD_IF_MATCH,
+ MHD_HTTP_HEADER_IF_NONE_MATCH
+ " header must include a base32-encoded SHA-512 hash");
+ }
+ }
+ /* validate signature */
+ {
+ struct ANASTASIS_UploadSignaturePS usp = {
+ .purpose.size = htonl (sizeof (usp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD),
+ .new_recovery_data_hash = puc->new_policy_upload_hash
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_ANASTASIS_POLICY_UPLOAD,
+ &usp,
+ &puc->account_sig.eddsa_sig,
+ &account_pub->pub))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_FORBIDDEN,
+ TALER_EC_ANASTASIS_POLICY_BAD_SIGNATURE,
+ ANASTASIS_HTTP_HEADER_POLICY_SIGNATURE);
+ }
+ }
+
+ {
+ 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;
+
+ if (1 != sscanf (long_poll_timeout_ms,
+ "%u",
+ &timeout))
+ {
+ 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);
+ }
+ }
+
+ /* check if the client insists on paying */
+ {
+ const char *req;
+ unsigned int years;
+
+ req = MHD_lookup_connection_value (connection,
+ MHD_GET_ARGUMENT_KIND,
+ "storage_duration");
+ if (NULL != req)
+ {
+ char dummy;
+
+ if (1 != sscanf (req,
+ "%u%c",
+ &years,
+ &dummy))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "storage_duration (must be non-negative number)");
+ }
+ }
+ else
+ {
+ years = 0;
+ }
+ puc->end_date = GNUNET_TIME_relative_to_absolute (
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
+ years));
+ }
+
+ /* get ready to hash (done here as we may go async for payments next) */
+ puc->hash_ctx = GNUNET_CRYPTO_hash_context_start ();
+
+ /* Check database to see if the transaction is permissible */
+ {
+ struct GNUNET_TIME_Relative rem;
+
+ rem = GNUNET_TIME_absolute_get_remaining (puc->end_date);
+ 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++;
+
+ if (puc->payment_identifier_provided)
+ {
+ /* check if payment identifier is valid (existing and paid) */
+ bool paid;
+ bool valid_counter;
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = db->check_payment_identifier (db->cls,
+ &puc->payment_identifier,
+ &paid,
+ &valid_counter);
+ if (qs < 0)
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+
+ if ( (! paid) ||
+ (! valid_counter) )
+ {
+ if (! valid_counter)
+ {
+ puc->payment_identifier_provided = false;
+ if (0 == puc->years_to_pay)
+ puc->years_to_pay = 1;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Too many uploads with this payment identifier, initiating fresh payment\n");
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Given payment identifier not known to be paid, initiating payment\n");
+ }
+ return prepare_payment (puc);
+ }
+ }
+
+ 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))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "No payment identifier, requesting payment\n");
+ return begin_payment (puc);
+ }
+ /* Cost is zero, fake "zero" payment having happened */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Policy upload is free, allowing upload without payment\n");
+ qs = db->record_recdoc_payment (db->cls,
+ account_pub,
+ AH_post_counter,
+ &puc->payment_identifier,
+ &AH_annual_fee);
+ if (qs <= 0)
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+ rel = GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_YEARS,
+ 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),
+ ANASTASIS_MAX_YEARS_STORAGE);
+ puc->paid_until = GNUNET_TIME_relative_to_absolute (rel);
+ qs = db->update_lifetime (db->cls,
+ account_pub,
+ &puc->payment_identifier,
+ puc->paid_until);
+ if (qs <= 0)
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+ }
+ }
+ }
+
+ /* Check if existing policy matches upload (and if, skip it) */
+ {
+ struct GNUNET_HashCode hc;
+ enum ANASTASIS_DB_AccountStatus as;
+ uint32_t version;
+ struct GNUNET_TIME_Absolute now;
+ struct GNUNET_TIME_Relative rem;
+
+ as = db->lookup_account (db->cls,
+ account_pub,
+ &puc->paid_until,
+ &hc,
+ &version);
+ now = GNUNET_TIME_absolute_get ();
+ if (puc->paid_until.abs_value_us < now.abs_value_us)
+ puc->paid_until = now;
+ rem = GNUNET_TIME_absolute_get_difference (puc->paid_until,
+ puc->end_date);
+ 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++;
+
+ if ( (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED == as) &&
+ (0 != puc->years_to_pay) )
+ {
+ /* user requested extension, force payment */
+ as = ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED;
+ }
+ switch (as)
+ {
+ case ANASTASIS_DB_ACCOUNT_STATUS_PAYMENT_REQUIRED:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Expiration too low, initiating payment\n");
+ return prepare_payment (puc);
+ case ANASTASIS_DB_ACCOUNT_STATUS_HARD_ERROR:
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+ case ANASTASIS_DB_ACCOUNT_STATUS_NO_RESULTS:
+ /* continue below */
+ break;
+ case ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED:
+ if (0 == GNUNET_memcmp (&hc,
+ &puc->new_policy_upload_hash))
+ {
+ /* Refuse upload: we already have that backup! */
+ struct MHD_Response *resp;
+ MHD_RESULT ret;
+ char version_s[14];
+
+ GNUNET_snprintf (version_s,
+ sizeof (version_s),
+ "%u",
+ (unsigned int) version);
+ resp = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ TALER_MHD_add_global_headers (resp);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (resp,
+ ANASTASIS_HTTP_HEADER_POLICY_VERSION,
+ version_s));
+ ret = MHD_queue_response (connection,
+ MHD_HTTP_NOT_MODIFIED,
+ resp);
+ GNUNET_break (MHD_YES == ret);
+ MHD_destroy_response (resp);
+ return ret;
+ }
+ break;
+ }
+ }
+ /* ready to begin! */
+ return MHD_YES;
+ }
+
+ if (NULL != puc->resp)
+ {
+ MHD_RESULT ret;
+
+ /* We generated a response asynchronously, queue that */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Returning asynchronously generated response with HTTP status %u\n",
+ puc->response_code);
+ ret = MHD_queue_response (connection,
+ puc->response_code,
+ puc->resp);
+ GNUNET_break (MHD_YES == ret);
+ MHD_destroy_response (puc->resp);
+ puc->resp = NULL;
+ return ret;
+ }
+
+ /* handle upload */
+ if (0 != *recovery_data_size)
+ {
+ /* check MHD invariant */
+ GNUNET_assert (puc->upload_off + *recovery_data_size <= puc->upload_size);
+ memcpy (&puc->upload[puc->upload_off],
+ recovery_data,
+ *recovery_data_size);
+ puc->upload_off += *recovery_data_size;
+ GNUNET_CRYPTO_hash_context_read (puc->hash_ctx,
+ recovery_data,
+ *recovery_data_size);
+ *recovery_data_size = 0;
+ return MHD_YES;
+ }
+
+ if ( (0 == puc->upload_off) &&
+ (0 != puc->upload_size) &&
+ (NULL == puc->resp) )
+ {
+ /* wait for upload */
+ return MHD_YES;
+ }
+
+ /* finished with upload, check hash */
+ if (NULL != puc->hash_ctx)
+ {
+ struct GNUNET_HashCode our_hash;
+
+ GNUNET_CRYPTO_hash_context_finish (puc->hash_ctx,
+ &our_hash);
+ puc->hash_ctx = NULL;
+ if (0 != GNUNET_memcmp (&our_hash,
+ &puc->new_policy_upload_hash))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_ANASTASIS_POLICY_INVALID_UPLOAD,
+ "Data uploaded does not match Etag promise");
+ }
+ }
+
+ /* store backup to database */
+ {
+ enum ANASTASIS_DB_StoreStatus ss;
+ uint32_t version = UINT32_MAX;
+ char version_s[14];
+ char expir_s[32];
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Uploading recovery document\n");
+ ss = db->store_recovery_document (db->cls,
+ &puc->account,
+ &puc->account_sig,
+ &puc->new_policy_upload_hash,
+ puc->upload,
+ puc->upload_size,
+ &puc->payment_identifier,
+ &version);
+ GNUNET_snprintf (version_s,
+ sizeof (version_s),
+ "%u",
+ (unsigned int) version);
+ GNUNET_snprintf (expir_s,
+ sizeof (expir_s),
+ "%llu",
+ (unsigned long long)
+ (puc->paid_until.abs_value_us
+ / GNUNET_TIME_UNIT_SECONDS.rel_value_us));
+ switch (ss)
+ {
+ case ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Storage request limit exceeded, requesting payment\n");
+ if (! puc->payment_identifier_provided)
+ {
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG,
+ &puc->payment_identifier,
+ sizeof (struct ANASTASIS_PaymentSecretP));
+ puc->payment_identifier_provided = true;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Also no payment identifier, requesting payment\n");
+ }
+ return begin_payment (puc);
+ case ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Policy store operation requires payment\n");
+ if (! puc->payment_identifier_provided)
+ {
+ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG,
+ &puc->payment_identifier,
+ sizeof (struct ANASTASIS_PaymentSecretP));
+ puc->payment_identifier_provided = true;
+ }
+ return begin_payment (puc);
+ case ANASTASIS_DB_STORE_STATUS_HARD_ERROR:
+ case ANASTASIS_DB_STORE_STATUS_SOFT_ERROR:
+ return TALER_MHD_reply_with_error (puc->con,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+ case ANASTASIS_DB_STORE_STATUS_NO_RESULTS:
+ {
+ /* database says nothing actually changed, 304 (could
+ theoretically happen if another equivalent upload succeeded
+ since we last checked!) */
+ struct MHD_Response *resp;
+ MHD_RESULT ret;
+
+ resp = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ TALER_MHD_add_global_headers (resp);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (resp,
+ "Anastasis-Version",
+ version_s));
+ ret = MHD_queue_response (connection,
+ MHD_HTTP_NOT_MODIFIED,
+ resp);
+ GNUNET_break (MHD_YES == ret);
+ MHD_destroy_response (resp);
+ return ret;
+ }
+ case ANASTASIS_DB_STORE_STATUS_SUCCESS:
+ /* generate main (204) standard success reply */
+ {
+ struct MHD_Response *resp;
+ MHD_RESULT ret;
+
+ resp = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ TALER_MHD_add_global_headers (resp);
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (resp,
+ ANASTASIS_HTTP_HEADER_POLICY_VERSION,
+ version_s));
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (resp,
+ ANASTASIS_HTTP_HEADER_POLICY_EXPIRATION,
+ expir_s));
+ ret = MHD_queue_response (connection,
+ MHD_HTTP_NO_CONTENT,
+ resp);
+ GNUNET_break (MHD_YES == ret);
+ MHD_destroy_response (resp);
+ return ret;
+ }
+ }
+ }
+ GNUNET_break (0);
+ return MHD_NO;
+}