paivana

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

paivana-httpd_pay.c (12685B)


      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 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 General Public License for more details.
     14 
     15      You should have received a copy of the GNU 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_error_codes.h>
     30 #include "paivana-httpd_cookie.h"
     31 #include "paivana-httpd_helper.h"
     32 #include "paivana-httpd_pay.h"
     33 
     34 struct PayRequest;
     35 #define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct PayRequest
     36 #include "taler/merchant/get-private-orders-ORDER_ID.h"
     37 
     38 
     39 /**
     40  * Handle for processing actual payment.
     41  */
     42 struct PayRequest
     43 {
     44 
     45   /**
     46    * Kept in a DLL while suspended.
     47    */
     48   struct PayRequest *next;
     49 
     50   /**
     51    * Kept in a DLL while suspended.
     52    */
     53   struct PayRequest *prev;
     54 
     55   /**
     56    * Connection we are handling.
     57    */
     58   struct MHD_Connection *connection;
     59 
     60   /**
     61    * Buffer for TALER_MHD_parse_post_json().
     62    */
     63   void *buffer;
     64 
     65   /**
     66    * Uploaded JSON body, NULL if none yet.
     67    */
     68   json_t *body;
     69 
     70   /**
     71    * Handle for our request to the merchant backend. This
     72    * struct is in the #ph_head DLL as long as @e co is non-NULL.
     73    */
     74   struct TALER_MERCHANT_GetPrivateOrderHandle *co;
     75 
     76   /**
     77    * Response to return, NULL if not yet determined.
     78    */
     79   struct MHD_Response *response;
     80 
     81   /**
     82    * ID of the order the client claims to have paid. Aliased
     83    * from @e body.
     84    */
     85   const char *order_id;
     86 
     87   /**
     88    * Website the order is supposed to have paid for. Aliased
     89    * from @e body.
     90    */
     91   const char *website;
     92 
     93   /**
     94    * Client-side nonce.
     95    */
     96   struct PAIVANA_Nonce nonce;
     97 
     98   /**
     99    * Expiration time of the cookie.
    100    */
    101   struct GNUNET_TIME_Timestamp cur_time;
    102 
    103   /**
    104    * HTTP status to return in combination with @e resp to the client.
    105    */
    106   unsigned int response_status;
    107 
    108 };
    109 
    110 
    111 /**
    112  * Head of DLL of suspended requests.
    113  */
    114 static struct PayRequest *ph_head;
    115 
    116 /**
    117  * Tail of DLL of suspended requests.
    118  */
    119 static struct PayRequest *ph_tail;
    120 
    121 
    122 void
    123 PAIVANA_HTTPD_payment_shutdown ()
    124 {
    125   while (NULL != ph_head)
    126   {
    127     struct PayRequest *ph = ph_head;
    128 
    129     if (NULL != ph->co)
    130     {
    131       TALER_MERCHANT_get_private_order_cancel (ph->co);
    132       ph->co = NULL;
    133     }
    134     GNUNET_CONTAINER_DLL_remove (ph_head,
    135                                  ph_tail,
    136                                  ph);
    137     MHD_resume_connection (ph->connection);
    138     /* Note: PAIVANA_HTTPD_payment_destroy()
    139        will be called by the owner of 'ph',
    140        no need to do it here! */
    141   }
    142 }
    143 
    144 
    145 struct PayRequest *
    146 PAIVANA_HTTPD_payment_create (struct MHD_Connection *connection)
    147 {
    148   struct PayRequest *ph;
    149 
    150   ph = GNUNET_new (struct PayRequest);
    151   ph->connection = connection;
    152   return ph;
    153 }
    154 
    155 
    156 /**
    157  * Check that the @a contract that was paid is reasonable for the
    158  * request in @a ph, that is that we would indeed consider this
    159  * contract to apply for the website and duration indicated
    160  * in @a ph. If it does not apply, a response must be set in
    161  * @a ph.
    162  *
    163  * @param[in,out] ph request to check
    164  * @param contract contract to check
    165  * @return true if the contract is good for the request,
    166  *   false if not and thus a response object was created in @a ph
    167  */
    168 static bool
    169 check_contract (struct PayRequest *ph,
    170                 const json_t *contract)
    171 {
    172   struct GNUNET_TIME_Timestamp max_time
    173     = GNUNET_TIME_UNIT_FOREVER_TS;
    174   const char *target = NULL;
    175   struct GNUNET_JSON_Specification spec[] = {
    176     GNUNET_JSON_spec_mark_optional (
    177       GNUNET_JSON_spec_string ("fulfillment_url",
    178                                &target),
    179       NULL),
    180     GNUNET_JSON_spec_mark_optional (
    181       GNUNET_JSON_spec_timestamp ("max_pickup_time",
    182                                   &max_time),
    183       NULL),
    184     GNUNET_JSON_spec_end ()
    185   };
    186   enum GNUNET_GenericReturnValue ret;
    187   const char *ename;
    188   unsigned int eline;
    189 
    190   ret = GNUNET_JSON_parse (contract,
    191                            spec,
    192                            &ename,
    193                            &eline);
    194   if (GNUNET_OK != ret)
    195   {
    196     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    197                 "Encountered contract with unexpected fields: %s@%u\n",
    198                 ename,
    199                 eline);
    200     /* For now, we tolerate this and just continue.
    201        This is a design decision that could be revised. */
    202     return true;
    203   }
    204   if ( (NULL != target) &&
    205        (0 != strcmp (target,
    206                      ph->website)) )
    207   {
    208     GNUNET_break_op (0);
    209     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_WRONG_ORDER,
    210                                          ph->order_id);
    211     ph->response_status = MHD_HTTP_CONFLICT;
    212     return false;
    213   }
    214   if (GNUNET_TIME_timestamp_cmp (ph->cur_time,
    215                                  >,
    216                                  max_time))
    217   {
    218     GNUNET_break_op (0);
    219     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_TOO_LATE,
    220                                          ph->order_id);
    221     ph->response_status = MHD_HTTP_GONE;
    222     return false;
    223   }
    224   return true;
    225 }
    226 
    227 
    228 /**
    229  * Handle response from the GET /private/orders/$ORDER_ID request.
    230  *
    231  * @param ph the payment request we are processing
    232  * @param osr response details
    233  */
    234 static void
    235 order_status_cb (struct PayRequest *ph,
    236                  const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
    237 {
    238   ph->co = NULL;
    239   GNUNET_CONTAINER_DLL_remove (ph_head,
    240                                ph_tail,
    241                                ph);
    242   MHD_resume_connection (ph->connection);
    243   TALER_MHD_daemon_trigger ();
    244   switch (osr->hr.http_status)
    245   {
    246   case MHD_HTTP_OK:
    247     if (TALER_MERCHANT_OSC_PAID != osr->details.ok.status)
    248     {
    249       GNUNET_break_op (0);
    250       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_PAYMENT_MISSING,
    251                                            ph->order_id);
    252       ph->response_status = MHD_HTTP_CONFLICT;
    253     }
    254     else
    255     {
    256       void *ca = NULL;
    257       size_t ca_len = 0;
    258       char *cookie;
    259       struct MHD_Response *resp;
    260 
    261       if (! check_contract (ph,
    262                             osr->details.ok.details.paid.contract_terms))
    263         return;
    264       /* If we cannot get the client address, we just
    265          use 0/NULL and log an error. */
    266       GNUNET_break (PAIVANA_HTTPD_get_client_address (ph->connection,
    267                                                       &ca,
    268                                                       &ca_len));
    269       cookie = PAIVANA_HTTPD_compute_cookie (ph->cur_time,
    270                                              ph->website,
    271                                              ca_len,
    272                                              ca);
    273       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    274                   "Client paid, setting cookie `%s'\n",
    275                   cookie);
    276       GNUNET_free (ca);
    277       resp = MHD_create_response_from_buffer (0,
    278                                               NULL,
    279                                               MHD_RESPMEM_PERSISTENT);
    280       GNUNET_assert (NULL != resp);
    281       GNUNET_assert (MHD_YES ==
    282                      MHD_add_response_header (resp,
    283                                               MHD_HTTP_HEADER_SET_COOKIE,
    284                                               cookie));
    285       GNUNET_assert (MHD_YES ==
    286                      MHD_add_response_header (resp,
    287                                               MHD_HTTP_HEADER_LOCATION,
    288                                               ph->website));
    289       GNUNET_free (cookie);
    290       TALER_MHD_add_global_headers (resp,
    291                                     false);
    292       ph->response = resp;
    293       ph->response_status = MHD_HTTP_SEE_OTHER;
    294     }
    295     break;
    296   case MHD_HTTP_FORBIDDEN:
    297     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_REFUSED,
    298                                          NULL);
    299     ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    300     break;
    301   case MHD_HTTP_NOT_FOUND:
    302     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_ORDER_UNKNOWN,
    303                                          ph->order_id);
    304     ph->response_status = MHD_HTTP_NOT_FOUND;
    305     break;
    306   default:
    307     {
    308       char code[20];
    309 
    310       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    311                   "Unexpected status code %u from backend\n",
    312                   osr->hr.http_status);
    313       GNUNET_snprintf (code,
    314                        sizeof (code),
    315                        "%u",
    316                        osr->hr.http_status);
    317       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_ERROR,
    318                                            code);
    319       ph->response_status = MHD_HTTP_BAD_GATEWAY;
    320     }
    321     break;
    322   }
    323 }
    324 
    325 
    326 enum MHD_Result
    327 PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
    328                               const char *upload_data,
    329                               size_t *upload_data_size)
    330 {
    331   if (NULL == ph->body)
    332   {
    333     enum GNUNET_GenericReturnValue ret;
    334 
    335     ret = TALER_MHD_parse_post_json (ph->connection,
    336                                      &ph->buffer,
    337                                      upload_data,
    338                                      upload_data_size,
    339                                      &ph->body);
    340     if (GNUNET_OK != ret)
    341       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    342     if (NULL == ph->body)
    343       return MHD_YES;
    344   }
    345   if (NULL != ph->response)
    346   {
    347     return MHD_queue_response (ph->connection,
    348                                ph->response_status,
    349                                ph->response);
    350   }
    351   if (NULL == ph->order_id)
    352   {
    353     struct GNUNET_JSON_Specification spec[] = {
    354       GNUNET_JSON_spec_string ("order_id",
    355                                &ph->order_id),
    356       GNUNET_JSON_spec_string ("website",
    357                                &ph->website),
    358       GNUNET_JSON_spec_timestamp ("cur_time",
    359                                   &ph->cur_time),
    360       GNUNET_JSON_spec_fixed_auto ("nonce",
    361                                    &ph->nonce),
    362       GNUNET_JSON_spec_end ()
    363     };
    364     enum GNUNET_GenericReturnValue ret;
    365 
    366     ret = TALER_MHD_parse_json_data (ph->connection,
    367                                      ph->body,
    368                                      spec);
    369     if (GNUNET_YES != ret)
    370       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    371   }
    372   GNUNET_assert (NULL == ph->co);
    373   ph->co = TALER_MERCHANT_get_private_order_create (PH_ctx,
    374                                                     PH_merchant_base_url,
    375                                                     ph->order_id);
    376   if (NULL == ph->co)
    377   {
    378     GNUNET_break (0);
    379     return TALER_MHD_reply_with_error (ph->connection,
    380                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    381                                        TALER_EC_PAIVANA_GET_ORDER_FAILED,
    382                                        ph->order_id);
    383   }
    384   {
    385     char *paivana_id;
    386 
    387     paivana_id = PAIVANA_HTTPD_compute_paivana_id (ph->cur_time,
    388                                                    ph->website,
    389                                                    &ph->nonce);
    390     GNUNET_assert (
    391       GNUNET_OK ==
    392       TALER_MERCHANT_get_private_order_set_options (
    393         ph->co,
    394         TALER_MERCHANT_get_private_order_option_session_id (
    395           paivana_id)));
    396     GNUNET_free (paivana_id);
    397   }
    398   GNUNET_CONTAINER_DLL_insert (ph_head,
    399                                ph_tail,
    400                                ph);
    401   MHD_suspend_connection (ph->connection);
    402   GNUNET_assert (TALER_EC_NONE ==
    403                  TALER_MERCHANT_get_private_order_start (ph->co,
    404                                                          &order_status_cb,
    405                                                          ph));
    406   return MHD_YES;
    407 }
    408 
    409 
    410 void
    411 PAIVANA_HTTPD_payment_destroy (struct PayRequest *ph)
    412 {
    413   TALER_MHD_parse_post_cleanup_callback (ph->buffer);
    414   if (NULL != ph->co)
    415   {
    416     TALER_MERCHANT_get_private_order_cancel (ph->co);
    417     GNUNET_CONTAINER_DLL_remove (ph_head,
    418                                  ph_tail,
    419                                  ph);
    420     ph->co = NULL;
    421   }
    422   if (NULL != ph->response)
    423   {
    424     MHD_destroy_response (ph->response);
    425     ph->response = NULL;
    426   }
    427   json_decref (ph->body);
    428   GNUNET_free (ph);
    429 }