paivana

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

paivana-httpd_pay.c (11709B)


      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    *
     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_free (ca);
    254       resp = MHD_create_response_from_buffer (0,
    255                                               NULL,
    256                                               MHD_RESPMEM_PERSISTENT);
    257       GNUNET_assert (MHD_YES ==
    258                      MHD_add_response_header (resp,
    259                                               MHD_HTTP_HEADER_SET_COOKIE,
    260                                               cookie));
    261       GNUNET_assert (MHD_YES ==
    262                      MHD_add_response_header (resp,
    263                                               MHD_HTTP_HEADER_LOCATION,
    264                                               ph->website));
    265       GNUNET_free (cookie);
    266       TALER_MHD_add_global_headers (resp,
    267                                     false);
    268       ph->response = resp;
    269       ph->response_status = MHD_HTTP_SEE_OTHER;
    270     }
    271     break;
    272   case MHD_HTTP_FORBIDDEN:
    273     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_REFUSED,
    274                                          NULL);
    275     ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    276     break;
    277   case MHD_HTTP_NOT_FOUND:
    278     ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_ORDER_UNKNOWN,
    279                                          ph->order_id);
    280     ph->response_status = MHD_HTTP_NOT_FOUND;
    281     break;
    282   default:
    283     {
    284       char code[20];
    285 
    286       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    287                   "Unexpected status code %u from backend\n",
    288                   osr->hr.http_status);
    289       GNUNET_snprintf (code,
    290                        sizeof (code),
    291                        "%u",
    292                        osr->hr.http_status);
    293       ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_ERROR,
    294                                            code);
    295       ph->response_status = MHD_HTTP_BAD_GATEWAY;
    296     }
    297     break;
    298   }
    299   GNUNET_CONTAINER_DLL_insert (ph_head,
    300                                ph_tail,
    301                                ph);
    302   MHD_resume_connection (ph->connection);
    303   TALER_MHD_daemon_trigger ();
    304 
    305 }
    306 
    307 
    308 enum MHD_Result
    309 PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
    310                               const char *upload_data,
    311                               size_t *upload_data_size)
    312 {
    313   if (NULL == ph->body)
    314   {
    315     enum GNUNET_GenericReturnValue ret;
    316 
    317     ret = TALER_MHD_parse_post_json (ph->connection,
    318                                      &ph->buffer,
    319                                      upload_data,
    320                                      upload_data_size,
    321                                      &ph->body);
    322     if (GNUNET_OK != ret)
    323       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    324     if (NULL == ph->body)
    325       return MHD_YES;
    326   }
    327   if (NULL != ph->response)
    328   {
    329     return MHD_queue_response (ph->connection,
    330                                ph->response_status,
    331                                ph->response);
    332   }
    333   if (NULL == ph->order_id)
    334   {
    335     struct GNUNET_JSON_Specification spec[] = {
    336       GNUNET_JSON_spec_string ("order_id",
    337                                &ph->order_id),
    338       GNUNET_JSON_spec_string ("website",
    339                                &ph->website),
    340       GNUNET_JSON_spec_timestamp ("cur_time",
    341                                   &ph->cur_time),
    342       GNUNET_JSON_spec_fixed_auto ("nonce",
    343                                    &ph->nonce),
    344       GNUNET_JSON_spec_end ()
    345     };
    346     enum GNUNET_GenericReturnValue ret;
    347 
    348     ret = TALER_MHD_parse_json_data (ph->connection,
    349                                      ph->body,
    350                                      spec);
    351     if (GNUNET_YES != ret)
    352       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    353   }
    354   GNUNET_assert (NULL == ph->co);
    355   ph->co = TALER_MERCHANT_get_private_order_create (PH_ctx,
    356                                                     PH_merchant_base_url,
    357                                                     ph->order_id);
    358   if (NULL == ph->co)
    359   {
    360     GNUNET_break (0);
    361     return TALER_MHD_reply_with_error (ph->connection,
    362                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    363                                        TALER_EC_PAIVANA_GET_ORDER_FAILED,
    364                                        ph->order_id);
    365   }
    366   {
    367     char *paivana_id;
    368 
    369     paivana_id = PAIVANA_HTTPD_compute_paivana_id (ph->cur_time,
    370                                                    ph->website,
    371                                                    &ph->nonce);
    372     GNUNET_assert (
    373       GNUNET_OK ==
    374       TALER_MERCHANT_get_private_order_set_options (
    375         ph->co,
    376         TALER_MERCHANT_get_private_order_option_session_id (
    377           paivana_id)));
    378     GNUNET_free (paivana_id);
    379   }
    380   GNUNET_CONTAINER_DLL_insert (ph_head,
    381                                ph_tail,
    382                                ph);
    383   MHD_suspend_connection (ph->connection);
    384   GNUNET_assert (TALER_EC_NONE ==
    385                  TALER_MERCHANT_get_private_order_start (ph->co,
    386                                                          &order_status_cb,
    387                                                          ph));
    388   return MHD_YES;
    389 }
    390 
    391 
    392 void
    393 PAIVANA_HTTPD_payment_destroy (struct PayRequest *ph)
    394 {
    395   TALER_MHD_parse_post_cleanup_callback (ph->buffer);
    396   if (NULL != ph->co)
    397     TALER_MERCHANT_get_private_order_cancel (ph->co);
    398   if (NULL != ph->response)
    399     MHD_destroy_response (ph->response);
    400   json_decref (ph->body);
    401   GNUNET_free (ph);
    402 }