/*
  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"
struct PolicyUploadContext;
#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE struct \
        PolicyUploadContext
#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct \
        PolicyUploadContext
#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>
#include <taler/merchant/post-private-orders.h>
#include <taler/merchant/get-private-orders-ORDER_ID.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_PostPrivateOrdersHandle *po;

  /**
   * Used while we are waiting payment.
   */
  struct TALER_MERCHANT_GetPrivateOrderHandle *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;

  /**
   * Creation time of the payment record named by @e payment_identifier,
   * zero if there is none.  Non-zero means the client is asking about a
   * payment we already know, so the answer is a repeated payment request
   * rather than a fresh order.
   */
  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_post_private_orders_cancel (puc->po);
      puc->po = NULL;
    }
    if (NULL != puc->cpo)
    {
      TALER_MERCHANT_get_private_order_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_post_private_orders_cancel (puc->po);
  if (NULL != puc->cpo)
    TALER_MERCHANT_get_private_order_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,
                                false);
  {
    char *hdr;
    const 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 (struct PolicyUploadContext *puc,
             const struct TALER_MERCHANT_PostPrivateOrdersResponse *por)
{
  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 = ANASTASIS_DB_do_insert_recdoc_payment (
    &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,
      "insert 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 (struct PolicyUploadContext *puc,
                  const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
{
  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 = ANASTASIS_DB_do_update_account_lifetime (
        &puc->account,
        &puc->payment_identifier,
        GNUNET_TIME_timestamp_get (),
        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,
        "do update account 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_get_private_order_create (AH_ctx,
                                                        AH_backend_url,
                                                        order_id);
    GNUNET_assert (NULL != puc->cpo);
    GNUNET_free (order_id);
    GNUNET_assert (
      GNUNET_OK ==
      TALER_MERCHANT_get_private_order_set_options (
        puc->cpo,
        TALER_MERCHANT_get_private_order_option_timeout (timeout)));
    GNUNET_assert (
      TALER_EC_NONE ==
      TALER_MERCHANT_get_private_order_start (puc->cpo,
                                              &check_payment_cb,
                                              puc));
  }
  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 enum MHD_Result
begin_payment (struct PolicyUploadContext *puc)
{
  json_t *order;

  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);
    if (NULL == order)
    {
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (puc->con,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE,
                                         "could not create order");
    }
  }
  /* Only link into the list once we are actually about to suspend: the list
     exists to resume suspended connections at shutdown, and cleanup_ctx()
     frees the context without unlinking it, so an error return above would
     otherwise leave a dangling entry behind. */
  GNUNET_CONTAINER_DLL_insert (puc_head,
                               puc_tail,
                               puc);
  MHD_suspend_connection (puc->con);
  puc->po = TALER_MERCHANT_post_private_orders_create (AH_ctx,
                                                       AH_backend_url,
                                                       order);
  GNUNET_assert (NULL != puc->po);
  GNUNET_assert (
    GNUNET_OK ==
    TALER_MERCHANT_post_private_orders_set_options (
      puc->po,
      TALER_MERCHANT_post_private_orders_option_create_token (false)));
  GNUNET_assert (TALER_EC_NONE ==
                 TALER_MERCHANT_post_private_orders_start (puc->po,
                                                           &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 enum MHD_Result
prepare_payment (struct PolicyUploadContext *puc)
{
  if (! puc->payment_identifier_provided)
  {
    GNUNET_CRYPTO_random_block (
      &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;
}


enum 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");
      }
    }
    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);
      }
    }

    /* Only now, with the account signature verified, do we allocate the
       upload buffer: sizing it from the client-declared Content-Length before
       checking who is asking lets anyone force repeated multi-megabyte
       allocations. */
    /* 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;
    }

    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)");
        }
        if (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 (exceeds maximum storage duration)");
        }
      }
      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 = false;
        bool valid_counter = false;
        enum GNUNET_DB_QueryStatus qs;

        qs = ANASTASIS_DB_get_recdoc_payment (
          &puc->payment_identifier,
          &paid,
          &valid_counter,
          &puc->existing_pi_timestamp);
        if (qs < 0)
          return TALER_MHD_reply_with_error (puc->con,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_GENERIC_DB_FETCH_FAILED,
                                             NULL);
        if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
        {
          /* The client named a payment identifier we have no record of.  The
             result specification was not touched in that case, so we must not
             look at @e paid or @e valid_counter; treat it as unpaid. */
          GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                      "Given payment identifier is unknown, initiating payment\n");
          puc->payment_identifier_provided = false;
          if (0 == puc->years_to_pay)
            puc->years_to_pay = 1;
          return prepare_payment (puc);
        }

        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 (&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 = ANASTASIS_DB_do_insert_recdoc_payment (
          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);
        qs = ANASTASIS_DB_do_update_account_lifetime (
          account_pub,
          &puc->payment_identifier,
          GNUNET_TIME_relative_to_timestamp (rel),
          GNUNET_TIME_UNIT_ZERO,
          &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 hash;
      enum ANASTASIS_DB_AccountStatus as;
      uint32_t version;
      struct GNUNET_TIME_Timestamp now;
      struct GNUNET_TIME_Relative rem;

      as = ANASTASIS_DB_get_account (
        account_pub,
        &puc->paid_until,
        &hash,
        &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 (&hash,
                                &puc->new_policy_upload_hash))
        {
          /* Refuse upload: we already have that backup! */
          struct MHD_Response *resp;
          enum 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,
                                        false);
          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)
  {
    enum 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 = ANASTASIS_DB_do_insert_recovery_document (
      &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 (&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 (&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;
        enum MHD_Result ret;

        resp = MHD_create_response_from_buffer (0,
                                                NULL,
                                                MHD_RESPMEM_PERSISTENT);
        TALER_MHD_add_global_headers (resp,
                                      false);
        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;
        enum MHD_Result ret;

        resp = MHD_create_response_from_buffer (0,
                                                NULL,
                                                MHD_RESPMEM_PERSISTENT);
        TALER_MHD_add_global_headers (resp,
                                      false);
        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;
}
