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 }