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 }