diff options
Diffstat (limited to 'src/backend/anastasis-httpd_policy-upload.c')
-rw-r--r-- | src/backend/anastasis-httpd_policy-upload.c | 1198 |
1 files changed, 1198 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..83e8117 --- /dev/null +++ b/src/backend/anastasis-httpd_policy-upload.c @@ -0,0 +1,1198 @@ +/* + This file is part of Anastasis + Copyright (C) 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 <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_SECONDS, 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; + + /** + * 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; + + /** + * 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_Timestamp 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_Timestamp end_date; + + /** + * How long is the account already valid? + * Determines how much the user needs to pay. + */ + struct GNUNET_TIME_Timestamp 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->meta_data); + GNUNET_free (puc); +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param[in,out] puc details about the operation + * @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_ERROR, + "Backend returned status %u/%d when trying to setup order\n", + por->hr.http_status, + (int) por->hr.ec); + puc->resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_SYNC_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_incref ("backend-reply", + (json_t *) por->hr.reply))); + puc->response_code = MHD_HTTP_BAD_GATEWAY; + 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 osr order status + */ +static void +check_payment_cb (void *cls, + 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; + 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_assert (MHD_HTTP_OK == hr->http_status); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment status checked: %d\n", + osr->details.ok.status); + 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); + 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 (! GNUNET_TIME_absolute_is_zero (puc->existing_pi_timestamp.abs_time)) + { + /* 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. + */ +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 */, + 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) +{ + static const char *no_uuids[1] = { NULL }; + 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)); + 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", + "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, + no_uuids, /* 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; + + 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 *metas; + + metas = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + ANASTASIS_HTTP_HEADER_POLICY_META_DATA); + if (NULL == metas) + { + 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"); + } + } + /* now setup 'puc' */ + { + 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"); + } + 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; + } + + 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; + + etag = MHD_lookup_connection_value (connection, + 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 + 1, + strlen (etag) - 2, + &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); + } + } + + 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 */ + { + 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 = 1; + } + puc->end_date = GNUNET_TIME_relative_to_timestamp ( + 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.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) + { + /* 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) + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Relative rel; + + /* generate fresh payment identifier */ + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_STRONG, + &puc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + if (! TALER_amount_is_zero (&AH_annual_fee)) + { + 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_TIME_relative2s (rel, + true), + ANASTASIS_MAX_YEARS_STORAGE); + puc->paid_until = GNUNET_TIME_relative_to_timestamp (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_Timestamp now; + struct GNUNET_TIME_Relative rem; + + as = db->lookup_account (db->cls, + account_pub, + &puc->paid_until, + &hc, + &version); + 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.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) ) + { + /* 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->meta_data, + puc->meta_data_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_time.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; +} |