/* 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 */ /** * @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 #include #include #include #include /** * 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[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_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 ( 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_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. */ 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; 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; } { /* 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; 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); } } /* 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; }