paivana

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

paivana-httpd_pay.c (11830B)


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