paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

paivana-httpd_pay.c (24772B)


      1 /*
      2      This file is part of GNUnet.
      3      Copyright (C) 2026 Taler Systems SA
      4 
      5      Paivana is free software; you can redistribute it and/or
      6      modify it under the terms of the GNU Affero General Public License
      7      as published by the Free Software Foundation; either version
      8      3, or (at your option) any later version.
      9 
     10      Paivana is distributed in the hope that it will be useful,
     11      but WITHOUT ANY WARRANTY; without even the implied warranty
     12      of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
     13      the GNU Affero General Public License for more details.
     14 
     15      You should have received a copy of the GNU Affero General Public
     16      License along with Paivana; see the file COPYING.  If not,
     17      write to the Free Software Foundation, Inc., 51 Franklin
     18      Street, Fifth Floor, Boston, MA 02110-1301, USA.
     19 */
     20 
     21 /**
     22  * @author Christian Grothoff
     23  * @file paivana-httpd_pay.c
     24  * @brief payment processing logic
     25  */
     26 #include <microhttpd.h>
     27 #include <gnunet/gnunet_util_lib.h>
     28 #include <taler/taler_mhd_lib.h>
     29 #include <taler/taler_json_lib.h>
     30 #include <taler/taler_error_codes.h>
     31 #include "paivana-httpd_cookie.h"
     32 #include "paivana-httpd_helper.h"
     33 #include "paivana-httpd_pay.h"
     34 
     35 struct PayRequest;
     36 #define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct PayRequest
     37 #include "taler/merchant/get-private-orders-ORDER_ID.h"
     38 
     39 /**
     40  * How long we give the merchant backend to answer the
     41  * `GET /private/orders/$ORDER_ID' behind one client's redemption.
     42  *
     43  * A bound is needed at all because the endpoint is unauthenticated:
     44  * every syntactically valid POST suspends an MHD connection and issues
     45  * a backend query before any payment has been shown to exist, so
     46  * without one, a wedged backend pins a suspended connection per
     47  * request forever.  MHD_OPTION_CONNECTION_TIMEOUT does not apply to
     48  * suspended connections, so the deadline has to come from us.
     49  *
     50  * It is passed as TALER_MERCHANT_get_private_order_option_timeout(),
     51  * which does two things at once: it caps the request client-side
     52  * (CURLOPT_TIMEOUT_MS, so an unreachable or hung backend is bounded
     53  * here too) and it sets `timeout_ms' on the URL, asking the backend to
     54  * long-poll for that long before reporting an order as unpaid.
     55  *
     56  * The long poll is wanted, not merely tolerated.  By the time the
     57  * client posts here it has already seen the backend confirm the
     58  * payment, so an order that still reads "unpaid" means either a client
     59  * that is lying -- replaying an order ID it never paid -- or one that
     60  * raced a state change that is about to land.  Waiting a few seconds
     61  * settles the race in the honest client's favour, and the dishonest
     62  * one pays for it with a bounded wait and then a 409.  Keep it short:
     63  * this is the interval an attacker can pin a connection for.
     64  */
     65 #define MERCHANT_ORDER_TIMEOUT \
     66         GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 5)
     67 
     68 
     69 /**
     70  * Handle for processing actual payment.
     71  */
     72 struct PayRequest
     73 {
     74 
     75   /**
     76    * Kept in a DLL while suspended.
     77    */
     78   struct PayRequest *next;
     79 
     80   /**
     81    * Kept in a DLL while suspended.
     82    */
     83   struct PayRequest *prev;
     84 
     85   /**
     86    * Connection we are handling.
     87    */
     88   struct MHD_Connection *connection;
     89 
     90   /**
     91    * Buffer for TALER_MHD_parse_post_json().
     92    */
     93   void *buffer;
     94 
     95   /**
     96    * Uploaded JSON body, NULL if none yet.
     97    */
     98   json_t *body;
     99 
    100   /**
    101    * Handle for our request to the merchant backend. This
    102    * struct is in the #ph_head DLL as long as @e co is non-NULL.
    103    */
    104   struct TALER_MERCHANT_GetPrivateOrderHandle *co;
    105 
    106   /**
    107    * Response to return, NULL if not yet determined.
    108    */
    109   struct MHD_Response *response;
    110 
    111   /**
    112    * ID of the order the client claims to have paid. Aliased
    113    * from @e body.
    114    */
    115   const char *order_id;
    116 
    117   /**
    118    * Website the order is supposed to have paid for. Aliased
    119    * from @e body.
    120    */
    121   const char *website;
    122 
    123   /**
    124    * Client-side nonce.
    125    */
    126   struct PAIVANA_Nonce nonce;
    127 
    128   /**
    129    * End of the access the client is redeeming: the expiration of the
    130    * cookie we mint, and one of the three inputs the client hashed into
    131    * the paivana_id the order was created under.  Chosen by the client
    132    * and bounded above by the contract's `max_pickup_time'.
    133    */
    134   struct GNUNET_TIME_Timestamp expiration;
    135 
    136   /**
    137    * HTTP status to return in combination with @e response to the
    138    * client.
    139    */
    140   unsigned int response_status;
    141 
    142 };
    143 
    144 
    145 /**
    146  * Head of DLL of suspended requests.
    147  */
    148 static struct PayRequest *ph_head;
    149 
    150 /**
    151  * Tail of DLL of suspended requests.
    152  */
    153 static struct PayRequest *ph_tail;
    154 
    155 
    156 void
    157 PAIVANA_HTTPD_payment_shutdown ()
    158 {
    159   while (NULL != ph_head)
    160   {
    161     struct PayRequest *ph = ph_head;
    162 
    163     if (NULL != ph->co)
    164     {
    165       TALER_MERCHANT_get_private_order_cancel (ph->co);
    166       ph->co = NULL;
    167     }
    168     GNUNET_CONTAINER_DLL_remove (ph_head,
    169                                  ph_tail,
    170                                  ph);
    171     MHD_resume_connection (ph->connection);
    172     /* Note: PAIVANA_HTTPD_payment_destroy()
    173        will be called by the owner of 'ph',
    174        no need to do it here! */
    175   }
    176 }
    177 
    178 
    179 struct PayRequest *
    180 PAIVANA_HTTPD_payment_create (struct MHD_Connection *connection)
    181 {
    182   struct PayRequest *ph;
    183 
    184   ph = GNUNET_new (struct PayRequest);
    185   ph->connection = connection;
    186   return ph;
    187 }
    188 
    189 
    190 /**
    191  * Is @a website a URL below our own base URL?
    192  *
    193  * Used to bound where a client may send itself once it has paid for
    194  * an order that carries no fulfillment URL of its own: without this
    195  * the client picks the redirect target and the site the access cookie
    196  * is minted for.
    197  *
    198  * The comparison is on whole path segments.  A bare prefix test would
    199  * accept "https://example.com.evil.net/" for a base URL of
    200  * "https://example.com", because strip_trailing_slashes() has removed
    201  * the '/' that used to terminate it.
    202  *
    203  * @param website candidate URL, from the client
    204  * @return true if @a website is our base URL or something below it
    205  */
    206 static bool
    207 under_our_base_url (const char *website)
    208 {
    209   size_t blen;
    210 
    211   if (NULL == PH_base_url)
    212   {
    213     /* BASE_URL is optional; without it we have nothing to compare
    214        against and must not guess.  Note that dereferencing it here
    215        used to be an unconditional crash. */
    216     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    217                 "Cannot check the target of an order without a"
    218                 " fulfillment URL: BASE_URL is not configured\n");
    219     return false;
    220   }
    221   blen = strlen (PH_base_url);
    222   if (0 != strncmp (website,
    223                     PH_base_url,
    224                     blen))
    225     return false;
    226   /* PH_base_url has no trailing '/' (strip_trailing_slashes()), so
    227      require the boundary here rather than inheriting it. */
    228   return ('\0' == website[blen]) ||
    229          ('/' == website[blen]);
    230 }
    231 
    232 
    233 /**
    234  * Check that the @a contract that was paid is reasonable for the
    235  * request in @a ph, that is that we would indeed consider this
    236  * contract to apply for the website and duration indicated
    237  * in @a ph. If it does not apply, a response must be set in
    238  * @a ph.
    239  *
    240  * @param[in,out] ph request to check
    241  * @param contract contract to check
    242  * @return true if the contract is good for the request,
    243  *   false if not and thus a response object was created in @a ph
    244  */
    245 static bool
    246 check_contract (struct PayRequest *ph,
    247                 const json_t *contract)
    248 {
    249   struct GNUNET_TIME_Timestamp max_time
    250     = GNUNET_TIME_UNIT_FOREVER_TS;
    251   const char *target = NULL;
    252   struct GNUNET_JSON_Specification spec[] = {
    253     GNUNET_JSON_spec_mark_optional (
    254       TALER_JSON_spec_web_url ("fulfillment_url",
    255                                &target),
    256       NULL),
    257     GNUNET_JSON_spec_mark_optional (
    258       GNUNET_JSON_spec_timestamp ("max_pickup_time",
    259                                   &max_time),
    260       NULL),
    261     GNUNET_JSON_spec_end ()
    262   };
    263   enum GNUNET_GenericReturnValue ret;
    264   const char *ename;
    265   unsigned int eline;
    266 
    267   ret = GNUNET_JSON_parse (contract,
    268                            spec,
    269                            &ename,
    270                            &eline);
    271   if (GNUNET_OK != ret)
    272   {
    273     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    274                 "Encountered contract with unexpected fields: %s@%u\n",
    275                 ename,
    276                 eline);
    277     /* Fail closed: returning true here would skip every check below --
    278        the fulfillment_url binding, the base-URL containment test and
    279        the max_pickup_time deadline -- and mint an access cookie for
    280        whatever website the client named.  Tolerating unknown *extra*
    281        fields is already what GNUNET_JSON_parse() does; a failure here
    282        means a field we do look at was malformed. */
    283     GNUNET_break_op (0);
    284     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_WRONG_ORDER,
    285                                          ph->order_id);
    286     ph->response_status = MHD_HTTP_CONFLICT;
    287     return false;
    288   }
    289   if ( (NULL != target) &&
    290        (0 != strcmp (target,
    291                      ph->website)) )
    292   {
    293     GNUNET_break_op (0);
    294     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_WRONG_ORDER,
    295                                          ph->order_id);
    296     ph->response_status = MHD_HTTP_CONFLICT;
    297     return false;
    298   }
    299   if ( ( (NULL == target) &&
    300          (! under_our_base_url (ph->website)) ) ||
    301        (! TALER_is_web_url (ph->website)) )
    302   {
    303     /* Bad: the order has no fulfillment URL, and on top of that
    304        the target given is not from our domain or not a well-formed
    305        URL. Reject hard. */
    306     GNUNET_break_op (0);
    307     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_INVALID_TARGET,
    308                                          ph->website);
    309     ph->response_status = MHD_HTTP_CONFLICT;
    310     return false;
    311   }
    312   if (GNUNET_TIME_timestamp_cmp (ph->expiration,
    313                                  >,
    314                                  max_time))
    315   {
    316     GNUNET_break_op (0);
    317     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_TOO_LATE,
    318                                          ph->order_id);
    319     ph->response_status = MHD_HTTP_GONE;
    320     return false;
    321   }
    322   return true;
    323 }
    324 
    325 
    326 /**
    327  * Handle response from the GET /private/orders/$ORDER_ID request.
    328  *
    329  * @param ph the payment request we are processing
    330  * @param osr response details
    331  */
    332 static void
    333 order_status_cb (struct PayRequest *ph,
    334                  const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
    335 {
    336   ph->co = NULL;
    337   GNUNET_CONTAINER_DLL_remove (ph_head,
    338                                ph_tail,
    339                                ph);
    340   MHD_resume_connection (ph->connection);
    341   TALER_MHD_daemon_trigger ();
    342   switch (osr->hr.http_status)
    343   {
    344   case MHD_HTTP_OK:
    345     /* "paid" survives a refund -- the merchant reports the refund in
    346        separate fields (api-merchant.rst, CheckPaymentPaidResponse) --
    347        so testing the status alone would hand a fresh cookie to someone
    348        who has had their money back. */
    349     if ( (TALER_MERCHANT_OSC_PAID != osr->details.ok.status) ||
    350          (osr->details.ok.details.paid.refunded) ||
    351          (osr->details.ok.details.paid.refund_pending) )
    352     {
    353       GNUNET_break_op (0);
    354       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_PAYMENT_MISSING,
    355                                            ph->order_id);
    356       ph->response_status = MHD_HTTP_CONFLICT;
    357     }
    358     else
    359     {
    360       void *ca = NULL;
    361       size_t ca_len = 0;
    362       char *cookie;
    363       struct MHD_Response *resp;
    364 
    365       if (! check_contract (ph,
    366                             osr->details.ok.details.paid.contract_terms))
    367         return;
    368       /* The client address is bound into the cookie MAC; computing
    369          the cookie over an empty address would produce a cookie that
    370          PAIVANA_HTTPD_check_cookie can never match, silently denying
    371          the access the client just paid for.  Treat failure to obtain
    372          it as a hard error instead.
    373 
    374          Note: This should become conditional once we add a
    375          configuration option to not include the client address in the
    376          cookie hash to allow one payment to be used from any IP
    377          address. */
    378       if (! PAIVANA_HTTPD_get_client_address (ph->connection,
    379                                               &ca,
    380                                               &ca_len))
    381       {
    382         GNUNET_break (0);
    383         ph->response = TALER_MHD_make_error (
    384           TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    385           ph->order_id);
    386         ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    387         break;
    388       }
    389       cookie = PAIVANA_HTTPD_compute_cookie (ph->expiration,
    390                                              ph->website,
    391                                              ca_len,
    392                                              ca);
    393       /* The cookie is the bearer credential proving payment; anyone
    394          who can read the log could replay it from the same address. */
    395       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    396                   "Client paid for `%s', setting access cookie\n",
    397                   ph->website);
    398       GNUNET_free (ca);
    399       resp = MHD_create_response_from_buffer (0,
    400                                               NULL,
    401                                               MHD_RESPMEM_PERSISTENT);
    402       GNUNET_assert (NULL != resp);
    403       if ( (MHD_YES !=
    404             MHD_add_response_header (resp,
    405                                      MHD_HTTP_HEADER_SET_COOKIE,
    406                                      cookie)) ||
    407            (MHD_YES !=
    408             MHD_add_response_header (resp,
    409                                      MHD_HTTP_HEADER_LOCATION,
    410                                      ph->website)) )
    411       {
    412         /* Neither header is optional: without the `Set-Cookie' the
    413            client has paid and been sent back to a page that will
    414            paywall it again, and without the `Location' the 303 has no
    415            target at all.  Answering 500 at least says so, and leaves
    416            the order paid and the redemption repeatable; sending the
    417            303 anyway does not. */
    418         GNUNET_break (0);
    419         MHD_destroy_response (resp);
    420         GNUNET_free (cookie);
    421         ph->response = TALER_MHD_make_error (
    422           TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    423           ph->website);
    424         ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    425         break;
    426       }
    427       GNUNET_free (cookie);
    428       TALER_MHD_add_global_headers (resp,
    429                                     false);
    430       ph->response = resp;
    431       ph->response_status = MHD_HTTP_SEE_OTHER;
    432     }
    433     break;
    434   case MHD_HTTP_UNAUTHORIZED:
    435   case MHD_HTTP_FORBIDDEN:
    436     /* Our `MERCHANT_ACCESS_TOKEN' is wrong: the operator's problem, not
    437        the client's, hence 500 and not a 4xx.  UNAUTHORIZED is the case
    438        that actually fires -- taler-merchant-httpd_auth.c answers a bad
    439        bearer token with 401 -- and without it this landed in the
    440        default branch below, telling the operator that a protocol
    441        incompatibility should be reported to us.
    442 
    443        Note that GANA has 9801 documented as a 502 and 9803 as a 500,
    444        i.e. the two the other way round from what is sent here and
    445        below.  The statuses are right: RFC 9110 section 15.6.3 gives
    446        502 for "an invalid response from an inbound server", which is
    447        the unexpected-status case (9803), while a bearer token of ours
    448        that the backend will not take is our own misconfiguration and
    449        not the upstream misbehaving (9801).  Fixing the registry is a
    450        change in gana, a different repository. */
    451     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    452                 "Merchant backend at `%s' rejected our credentials (HTTP"
    453                 " %u); check MERCHANT_ACCESS_TOKEN\n",
    454                 PH_merchant_base_url,
    455                 osr->hr.http_status);
    456     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_REFUSED,
    457                                          NULL);
    458     ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    459     break;
    460   case MHD_HTTP_NOT_FOUND:
    461     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_ORDER_UNKNOWN,
    462                                          ph->order_id);
    463     ph->response_status = MHD_HTTP_NOT_FOUND;
    464     break;
    465   case 0:
    466     /* No HTTP status at all.  The merchant client library reports this
    467        both for a request that never completed and for one whose reply
    468        it could not make sense of, with the same error code; @e reply is
    469        what tells them apart, being NULL only in the former case. */
    470     if (NULL != osr->hr.reply)
    471     {
    472       GNUNET_break_op (0);
    473       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    474                   "Merchant backend at `%s' sent an unusable reply for"
    475                   " order `%s'\n",
    476                   PH_merchant_base_url,
    477                   ph->order_id);
    478       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_ERROR,
    479                                            ph->order_id);
    480       ph->response_status = MHD_HTTP_BAD_GATEWAY;
    481       break;
    482     }
    483     /* Nothing came back: the request hit #MERCHANT_ORDER_TIMEOUT, or
    484        the backend was unreachable.  Either way the client has waited
    485        for us rather than been answered, which is what separates 504
    486        from the 502 above. */
    487     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    488                 "Merchant backend at `%s' did not answer for order `%s'"
    489                 " within %s; giving up on this redemption\n",
    490                 PH_merchant_base_url,
    491                 ph->order_id,
    492                 GNUNET_STRINGS_relative_time_to_string (MERCHANT_ORDER_TIMEOUT,
    493                                                         true));
    494     /* GENERIC_TIMEOUT's hint ("trying again might help") is the one
    495        that is true for the client here; GET_ORDER_FAILED says "this
    496        should never happen, consult the logs", which is wrong advice
    497        for a backend that was merely slow.  A dedicated
    498        PAIVANA_BACKEND_TIMEOUT would be better still, but that is a
    499        GANA registration and so its own change in another
    500        repository. */
    501     ph->response = TALER_MHD_make_error (TALER_EC_GENERIC_TIMEOUT,
    502                                          ph->order_id);
    503     ph->response_status = MHD_HTTP_GATEWAY_TIMEOUT;
    504     break;
    505   default:
    506     {
    507       char code[20];
    508 
    509       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    510                   "Unexpected status code %u from backend\n",
    511                   osr->hr.http_status);
    512       GNUNET_snprintf (code,
    513                        sizeof (code),
    514                        "%u",
    515                        osr->hr.http_status);
    516       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_ERROR,
    517                                            code);
    518       ph->response_status = MHD_HTTP_BAD_GATEWAY;
    519     }
    520     break;
    521   }
    522 }
    523 
    524 
    525 enum MHD_Result
    526 PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
    527                               const char *upload_data,
    528                               size_t *upload_data_size)
    529 {
    530   if (NULL == ph->body)
    531   {
    532     enum GNUNET_GenericReturnValue ret;
    533 
    534     ret = TALER_MHD_parse_post_json (ph->connection,
    535                                      &ph->buffer,
    536                                      upload_data,
    537                                      upload_data_size,
    538                                      &ph->body);
    539     if (GNUNET_OK != ret)
    540       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    541     if (NULL == ph->body)
    542       return MHD_YES;
    543   }
    544   if (NULL != ph->response)
    545   {
    546     return MHD_queue_response (ph->connection,
    547                                ph->response_status,
    548                                ph->response);
    549   }
    550   if (NULL == ph->order_id)
    551   {
    552     struct GNUNET_JSON_Specification spec[] = {
    553       TALER_JSON_spec_slug ("order_id",
    554                             &ph->order_id),
    555       TALER_JSON_spec_web_url ("website",
    556                                &ph->website),
    557       GNUNET_JSON_spec_timestamp ("expiration",
    558                                   &ph->expiration),
    559       GNUNET_JSON_spec_fixed_auto ("nonce",
    560                                    &ph->nonce),
    561       GNUNET_JSON_spec_end ()
    562     };
    563     enum GNUNET_GenericReturnValue ret;
    564 
    565     ret = TALER_MHD_parse_json_data (ph->connection,
    566                                      ph->body,
    567                                      spec);
    568     if (GNUNET_YES != ret)
    569       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    570     /* `website' is a URL like any other paivana handles, and every
    571        other one is refused past #PH_MAX_URL_LENGTH before anything
    572        looks at it.  This one arrives in a JSON body instead of a
    573        request line, which is the only reason it escaped that: from
    574        here it goes into an HKDF, into a `Location' and into the
    575        `Path' of a `Set-Cookie', where three bytes of header are spent
    576        per byte of path.  Bound it in the same place and at the same
    577        length. */
    578     if (PH_MAX_URL_LENGTH < strlen (ph->website))
    579     {
    580       GNUNET_break_op (0);
    581       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    582                   "Refusing %llu byte `website' in payment redemption\n",
    583                   (unsigned long long) strlen (ph->website));
    584       return TALER_MHD_reply_with_error (
    585         ph->connection,
    586         MHD_HTTP_BAD_REQUEST,
    587         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    588         "website");
    589     }
    590     /* `expiration' is the end of the access being bought, not a
    591        statement about the client's clock: it is what the cookie's
    592        Max-Age is computed from, what check_cookie() enforces, and what
    593        is hashed into the paivana_id the order was created under, so we
    594        cannot re-derive it here even if we wanted to.  An expiration in
    595        the past would mint a cookie that is dead on arrival; the upper
    596        bound is the contract's `max_pickup_time', enforced in
    597        check_contract() once we have the contract to compare against. */
    598     if (GNUNET_TIME_absolute_is_past (ph->expiration.abs_time))
    599     {
    600       GNUNET_break_op (0);
    601       return TALER_MHD_reply_with_error (
    602         ph->connection,
    603         MHD_HTTP_BAD_REQUEST,
    604         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    605         "expiration");
    606     }
    607   }
    608   GNUNET_assert (NULL == ph->co);
    609   ph->co = TALER_MERCHANT_get_private_order_create (PH_merchant_ctx,
    610                                                     PH_merchant_base_url,
    611                                                     ph->order_id);
    612   if (NULL == ph->co)
    613   {
    614     GNUNET_break (0);
    615     return TALER_MHD_reply_with_error (ph->connection,
    616                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    617                                        TALER_EC_PAIVANA_GET_ORDER_FAILED,
    618                                        ph->order_id);
    619   }
    620   {
    621     char *paivana_id;
    622 
    623     paivana_id = PAIVANA_HTTPD_compute_paivana_id (ph->expiration,
    624                                                    ph->website,
    625                                                    &ph->nonce);
    626     GNUNET_assert (
    627       GNUNET_OK ==
    628       TALER_MERCHANT_get_private_order_set_options (
    629         ph->co,
    630         TALER_MERCHANT_get_private_order_option_session_id (
    631           paivana_id),
    632         TALER_MERCHANT_get_private_order_option_timeout (
    633           MERCHANT_ORDER_TIMEOUT)));
    634     GNUNET_free (paivana_id);
    635   }
    636   GNUNET_CONTAINER_DLL_insert (ph_head,
    637                                ph_tail,
    638                                ph);
    639   MHD_suspend_connection (ph->connection);
    640   {
    641     enum TALER_ErrorCode ec;
    642 
    643     ec = TALER_MERCHANT_get_private_order_start (ph->co,
    644                                                  &order_status_cb,
    645                                                  ph);
    646     if (TALER_EC_NONE != ec)
    647     {
    648       /* Everything the callee can fail on here is a resource failure
    649          it recovers from by telling us: curl_easy_init() or
    650          curl_multi_add_handle() came back empty.  That is one client's
    651          redemption going wrong, and asserting on it took the daemon
    652          down with every other request in flight, paid ones included. */
    653       GNUNET_break (0);
    654       GNUNET_CONTAINER_DLL_remove (ph_head,
    655                                    ph_tail,
    656                                    ph);
    657       MHD_resume_connection (ph->connection);
    658       TALER_MERCHANT_get_private_order_cancel (ph->co);
    659       ph->co = NULL;
    660       return TALER_MHD_reply_with_error (ph->connection,
    661                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    662                                          TALER_EC_PAIVANA_GET_ORDER_FAILED,
    663                                          ph->order_id);
    664     }
    665   }
    666   return MHD_YES;
    667 }
    668 
    669 
    670 void
    671 PAIVANA_HTTPD_payment_destroy (struct PayRequest *ph)
    672 {
    673   TALER_MHD_parse_post_cleanup_callback (ph->buffer);
    674   if (NULL != ph->co)
    675   {
    676     TALER_MERCHANT_get_private_order_cancel (ph->co);
    677     GNUNET_CONTAINER_DLL_remove (ph_head,
    678                                  ph_tail,
    679                                  ph);
    680     ph->co = NULL;
    681   }
    682   if (NULL != ph->response)
    683   {
    684     MHD_destroy_response (ph->response);
    685     ph->response = NULL;
    686   }
    687   json_decref (ph->body);
    688   GNUNET_free (ph);
    689 }