commit ba6d5c2a38a29e928e0878c2b8130b761040a0d0
parent e90465d04d6ef2d32a7ea0d920a2cb8cbfdc7d4a
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 19 Apr 2026 23:13:16 +0200
more work on Paivana: add pay handler
Diffstat:
8 files changed, 784 insertions(+), 225 deletions(-)
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am
@@ -11,14 +11,18 @@ bin_PROGRAMS = \
paivana_httpd_SOURCES = \
paivana-httpd.c \
+ paivana-httpd_cookie.c paivana-httpd_cookie.h \
paivana-httpd_reverse.c paivana-httpd_reverse.h \
+ paivana-httpd_pay.c paivana-httpd_pay.h \
paivana_pd.c paivana_pd.h
paivana_httpd_LDADD = \
$(LIBGCRYPT_LIBS) \
+ -ltalermerchant \
-ltalermhd \
+ -lgnunetjson \
+ -lgnunetcurl \
+ -lgnunetutil \
-lmicrohttpd \
-lcurl \
-ljansson \
- -lgnunetutil \
- -lgnunetjson \
-lz
diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c
@@ -29,8 +29,11 @@
#include "platform.h"
#include <curl/curl.h>
#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
#include <taler/taler_mhd_lib.h>
#include "paivana-httpd.h"
+#include "paivana-httpd_cookie.h"
+#include "paivana-httpd_pay.h"
#include "paivana-httpd_reverse.h"
#include "paivana_pd.h"
@@ -39,18 +42,51 @@
struct RequestContext
{
+
+ /**
+ * HTTP connection to the client.
+ */
+ struct MHD_Connection *connection;
+
/**
* Handle for request forwarding as reverse proxy.
*/
struct HttpRequest *hr;
/**
+ * Handle for processing actual payment.
+ */
+ struct PayRequest *hp;
+
+ /**
+ * Full request URL.
+ */
+ char *url;
+
+ /**
+ * True if this is a POST to the .well-known/paivana endpoint.
+ */
+ bool is_paivana;
+
+ /**
* We are past the paywall, forward to client.
*/
bool do_forward;
};
+char *PH_target_server_base_url;
+
+char *PH_merchant_base_url;
+
+struct GNUNET_CURL_Context *PH_ctx;
+
+/**
+ * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule().
+ */
+struct GNUNET_CURL_RescheduleContext *ctx_rc;
+
+
/**
* Set to true if we started a daemon.
*/
@@ -76,142 +112,6 @@ static int no_check;
*/
static int global_ret;
-char *target_server_base_url;
-
-/**
- * Merchant backend base URL.
- */
-static char *merchant_base_url;
-
-/**
- * Merchant backend access token.
- */
-static char *merchant_access_token;
-
-/**
- * Secret for the cookie generation.
- */
-static struct GNUNET_HashCode paivana_secret;
-
-
-/* ********************* Paivana Cookie handling ****************** */
-
-/**
- * Compute access cookie hash for the given @a expiration and @a ca.
- *
- * @param expiration expiration time of the cookie
- * @param ca_len number of bytes in @a ca
- * @param ca client address
- * @param[out] c set to the cookie hash
- */
-static void
-compute_cookie_hash (struct GNUNET_TIME_Timestamp expiration,
- size_t ca_len,
- const void *ca,
- struct GNUNET_HashCode *c)
-{
- struct GNUNET_TIME_AbsoluteNBO e;
-
- e = GNUNET_TIME_absolute_hton (expiration.abs_time);
- GNUNET_assert (GNUNET_YES ==
- GNUNET_CRYPTO_hkdf_gnunet (
- c, /* result */
- sizeof (c),
- &e, /* salt */
- sizeof (e),
- &paivana_secret, /* source key material */
- sizeof (paivana_secret),
- GNUNET_CRYPTO_kdf_arg (ca,
- ca_len)));
-}
-
-
-/**
- * Check if the given cookie currently grants access.
- *
- * @param cookie the cookie
- * @param ca_len number of bytes in @a ca
- * @param ca client address
- * @return true if the cookie is OK
- */
-static bool
-check_cookie (const char *cookie,
- size_t ca_len,
- const void *ca)
-{
- const char *dash;
- unsigned long long u;
- struct GNUNET_HashCode h;
- struct GNUNET_HashCode c;
- struct GNUNET_TIME_Timestamp a;
-
- dash = strchr (cookie,
- '-');
- if (NULL == dash)
- return false;
- dash++;
- if (1 !=
- sscanf (cookie,
- "%llu-",
- &u))
- return false;
- a.abs_time.abs_value_us = u * 1000LLU * 1000LLU;
- if (GNUNET_TIME_absolute_is_past (a.abs_time))
- return false;
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (dash,
- strlen (dash),
- &c,
- sizeof (c)))
- return false;
- compute_cookie_hash (a,
- ca_len,
- ca,
- &h);
- return (0 ==
- GNUNET_memcmp (&c,
- &h));
-}
-
-
-/**
- * Compute access cookie hash for the given @a expiration and @a ca.
- *
- * @param expiration expiration time of the cookie
- * @param ca_len number of bytes in @a ca
- * @param ca client address
- * @param[out] c set to the cookie hash
- */
-static char *
-compute_cookie (struct GNUNET_TIME_Timestamp expiration,
- size_t ca_len,
- const void *ca)
-{
- struct GNUNET_HashCode h;
- char *end;
- char cstr[128];
- char *res;
-
- compute_cookie_hash (expiration,
- ca_len,
- ca,
- &h);
- end = GNUNET_STRINGS_data_to_string (&h,
- sizeof (h),
- cstr,
- sizeof (cstr));
- *end = '\0';
- GNUNET_asprintf (
- &res,
- "%llu-%s",
- (unsigned long long) (expiration.abs_time.abs_value_us / 1000LLU / 1000LLU),
- cstr);
- return res;
-}
-
-
-/* *************** MHD response generation ***************** */
-
/**
* Main MHD callback for handling requests.
@@ -248,51 +148,79 @@ create_response (void *cls,
void **con_cls)
{
struct RequestContext *rc = *con_cls;
- struct HttpRequest *hr = rc->hr;
+ const char *cookie;
(void) cls;
- if (NULL == hr)
+ if ( (! rc->is_paivana) &&
+ (0 == strcmp (url,
+ ".well-known/paivana")) &&
+ (0 == strcasecmp (meth,
+ "POST")) )
{
- GNUNET_break (0);
- return MHD_NO;
+ rc->is_paivana = true;
+ }
+ if (rc->is_paivana)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Client POSTed payment, checking validity\n");
+ if (NULL == rc->hp)
+ rc->hp = PAIVANA_HTTPD_payment_create (rc->connection);
+ return PAIVANA_HTTPD_payment_handle (rc->hp,
+ upload_data,
+ upload_data_size);
}
- // FIXME: check if url is one that we reverse proxy!
+ // FIXME: check if url is one that we require payment for,
+ // if not set 'do_forward = true'.
+ // (also should eventually determine WHICH payment template
+ // we use...)
- if (! rc->do_forward)
+ if (rc->do_forward)
{
- const char *cookie;
- bool ok = (0 != no_check);
+ if (NULL == rc->hr)
+ rc->hr = PAIVANA_HTTPD_reverse_create (rc->connection,
+ rc->url);
+ return PAIVANA_HTTPD_reverse (rc->hr,
+ con,
+ url,
+ meth,
+ ver,
+ upload_data,
+ upload_data_size);
+ }
- cookie = MHD_lookup_connection_value (con,
- MHD_COOKIE_KIND,
- "Paivana-Cookie");
- if (NULL != cookie)
+ cookie = MHD_lookup_connection_value (con,
+ MHD_COOKIE_KIND,
+ "Paivana-Cookie");
+ if (NULL != cookie)
+ {
+ const union MHD_ConnectionInfo *ci;
+ const struct sockaddr *ca;
+ socklen_t ca_len;
+ bool ok;
+
+ // FIXME: de-duplicate with logic in paivana-httpd_pay.c,
+ // and also support getting client address from HTTP
+ // headers instead (in case of reverse proxy).
+ ci = MHD_get_connection_info (con,
+ MHD_CONNECTION_INFO_CLIENT_ADDRESS);
+ GNUNET_assert (NULL != ci);
+ ca = ci->client_addr;
+ switch (ca->sa_family)
{
- const union MHD_ConnectionInfo *ci;
- const struct sockaddr *ca;
- socklen_t ca_len;
-
- ci = MHD_get_connection_info (con,
- MHD_CONNECTION_INFO_CLIENT_ADDRESS);
- GNUNET_assert (NULL != ci);
- ca = ci->client_addr;
- switch (ca->sa_family)
- {
- case AF_INET:
- ca_len = sizeof (struct sockaddr_in);
- break;
- case AF_INET6:
- ca_len = sizeof (struct sockaddr_in6);
- break;
- default:
- GNUNET_break (0);
- ca_len = 0;
- break;
- }
- ok = check_cookie (cookie,
- ca_len,
- ca);
+ case AF_INET:
+ ca_len = sizeof (struct sockaddr_in);
+ break;
+ case AF_INET6:
+ ca_len = sizeof (struct sockaddr_in6);
+ break;
+ default:
+ GNUNET_break (0);
+ ca_len = 0;
+ break;
}
+ ok = PAIVANA_HTTPD_check_cookie (cookie,
+ ca_len,
+ ca);
if (! ok)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
@@ -301,26 +229,15 @@ create_response (void *cls,
MHD_HTTP_PAYMENT_REQUIRED,
paywall);
}
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Request ok!\n");
- rc->do_forward = true;
- /* TODO: hacks for 100 continue suppression should go here! */
- return MHD_YES;
}
-
- return PAIVANA_HTTPD_reverse (hr,
- con,
- url,
- meth,
- ver,
- upload_data,
- upload_data_size);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Request ok!\n");
+ rc->do_forward = true;
+ /* TODO: hacks for 100 continue suppression should go here! */
+ return MHD_YES;
}
-/* ************ MHD HTTP setup and event loop *************** */
-
-
/**
* Function called when MHD decides that we
* are done with a request.
@@ -346,9 +263,14 @@ mhd_completed_cb (void *cls,
return;
if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe)
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "MHD encountered error handling request: %d\n",
+ "MHD encountered error handling request to %s: %d\n",
+ rc->url,
toe);
- PAIVANA_HTTPD_reverse_cleanup (rc->hr);
+ if (NULL != rc->hr)
+ PAIVANA_HTTPD_reverse_cleanup (rc->hr);
+ if (NULL != rc->hp)
+ PAIVANA_HTTPD_payment_destroy (rc->hp);
+ GNUNET_free (rc->url);
GNUNET_free (rc);
*con_cls = NULL;
}
@@ -373,22 +295,11 @@ mhd_log_callback (void *cls,
struct MHD_Connection *connection)
{
struct RequestContext *rc;
- const union MHD_ConnectionInfo *ci;
- (void) cls;
- ci = MHD_get_connection_info (connection,
- MHD_CONNECTION_INFO_SOCKET_CONTEXT);
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Processing %s\n",
- url);
- if (NULL == ci)
- {
- GNUNET_break (0);
- return NULL;
- }
rc = GNUNET_new (struct RequestContext);
- rc->hr = PAIVANA_HTTPD_reverse_create (connection,
- url);
+ rc->connection = connection;
+ rc->url = GNUNET_strdup (url);
+ rc->do_forward = (1 == no_check);
return rc;
}
@@ -408,9 +319,21 @@ do_shutdown (void *cls)
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Shutting down...\n");
TALER_MHD_daemons_halt ();
+ PAIVANA_HTTPD_payment_shutdown ();
PAIVANA_HTTPD_reverse_shutdown ();
TALER_MHD_daemons_destroy ();
- GNUNET_free (target_server_base_url);
+ GNUNET_free (PH_target_server_base_url);
+ GNUNET_free (PH_merchant_base_url);
+ if (NULL != PH_ctx)
+ {
+ GNUNET_CURL_fini (PH_ctx);
+ PH_ctx = NULL;
+ }
+ if (NULL != ctx_rc)
+ {
+ GNUNET_CURL_gnunet_rc_destroy (ctx_rc);
+ ctx_rc = NULL;
+ }
}
@@ -548,7 +471,7 @@ run (void *cls,
c,
"paivana",
"DESTINATION_BASE_URL",
- &target_server_base_url))
+ &PH_target_server_base_url))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"paivana",
@@ -561,7 +484,7 @@ run (void *cls,
c,
"paivana",
"MERCHANT_BACKEND_URL",
- &merchant_base_url))
+ &PH_merchant_base_url))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"paivana",
@@ -573,19 +496,6 @@ run (void *cls,
GNUNET_CONFIGURATION_get_value_string (
c,
"paivana",
- "MERCHANT_ACCESS_TOKEN",
- &merchant_access_token))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "paivana",
- "MERCHANT_ACCESS_TOKEN");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- GNUNET_CONFIGURATION_get_value_string (
- c,
- "paivana",
"SECRET",
&secret))
{
@@ -605,6 +515,36 @@ run (void *cls,
}
GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
NULL);
+ PH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
+ &ctx_rc);
+ {
+ char *merchant_access_token;
+ char *auth_header;
+
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (
+ c,
+ "paivana",
+ "MERCHANT_ACCESS_TOKEN",
+ &merchant_access_token))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "MERCHANT_ACCESS_TOKEN");
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ GNUNET_asprintf (&auth_header,
+ "%s: %s",
+ MHD_HTTP_HEADER_AUTHORIZATION,
+ merchant_access_token);
+ GNUNET_free (merchant_access_token);
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_CURL_append_header (PH_ctx,
+ auth_header));
+ GNUNET_free (auth_header);
+ }
+ ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_ctx);
ret = TALER_MHD_listen_bind (c,
"paivana",
diff --git a/src/backend/paivana-httpd.h b/src/backend/paivana-httpd.h
@@ -40,7 +40,17 @@
* Destination to which HTTP server we forward requests to.
* Of the format "http://servername:PORT"
*/
-extern char *target_server_base_url;
+extern char *PH_target_server_base_url;
+
+/**
+ * Merchant backend base URL.
+ */
+extern char *PH_merchant_base_url;
+
+/**
+ * Curl context for making HTTP requests.
+ */
+extern struct GNUNET_CURL_Context *PH_ctx;
#endif
diff --git a/src/backend/paivana-httpd_cookie.c b/src/backend/paivana-httpd_cookie.c
@@ -0,0 +1,137 @@
+/*
+ This file is part of GNU Taler
+ Copyright (C) 2012-2014 GNUnet e.V.
+ Copyright (C) 2018, 2025 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version
+ 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with GNU Taler; see the file COPYING. If not,
+ write to the Free Software Foundation, Inc., 51 Franklin
+ Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/**
+ * @author Martin Schanzenbach
+ * @author Christian Grothoff
+ * @author Marcello Stanisci
+ * @file src/backend/paivana-httpd_cookie.c
+ * @brief Cookie computation logic for paivana
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <taler/taler_mhd_lib.h>
+#include "paivana-httpd_cookie.h"
+
+
+/**
+ * Secret for the cookie generation.
+ */
+struct GNUNET_HashCode paivana_secret;
+
+
+/**
+ * Compute access cookie hash for the given @a expiration and @a ca.
+ *
+ * @param expiration expiration time of the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @param[out] c set to the cookie hash
+ */
+static void
+compute_cookie_hash (struct GNUNET_TIME_Timestamp expiration,
+ size_t ca_len,
+ const void *ca,
+ struct GNUNET_HashCode *c)
+{
+ struct GNUNET_TIME_AbsoluteNBO e;
+
+ e = GNUNET_TIME_absolute_hton (expiration.abs_time);
+ GNUNET_assert (GNUNET_YES ==
+ GNUNET_CRYPTO_hkdf_gnunet (
+ c, /* result */
+ sizeof (c),
+ &e, /* salt */
+ sizeof (e),
+ &paivana_secret, /* source key material */
+ sizeof (paivana_secret),
+ GNUNET_CRYPTO_kdf_arg (ca,
+ ca_len)));
+}
+
+
+bool
+PAIVANA_HTTPD_check_cookie (const char *cookie,
+ size_t ca_len,
+ const void *ca)
+{
+ const char *dash;
+ unsigned long long u;
+ struct GNUNET_HashCode h;
+ struct GNUNET_HashCode c;
+ struct GNUNET_TIME_Timestamp a;
+
+ dash = strchr (cookie,
+ '-');
+ if (NULL == dash)
+ return false;
+ dash++;
+ if (1 !=
+ sscanf (cookie,
+ "%llu-",
+ &u))
+ return false;
+ a.abs_time.abs_value_us = u * 1000LLU * 1000LLU;
+ if (GNUNET_TIME_absolute_is_past (a.abs_time))
+ return false;
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (dash,
+ strlen (dash),
+ &c,
+ sizeof (c)))
+ return false;
+ compute_cookie_hash (a,
+ ca_len,
+ ca,
+ &h);
+ return (0 ==
+ GNUNET_memcmp (&c,
+ &h));
+}
+
+
+char *
+PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp expiration,
+ size_t ca_len,
+ const void *ca)
+{
+ struct GNUNET_HashCode h;
+ char *end;
+ char cstr[128];
+ char *res;
+
+ compute_cookie_hash (expiration,
+ ca_len,
+ ca,
+ &h);
+ end = GNUNET_STRINGS_data_to_string (&h,
+ sizeof (h),
+ cstr,
+ sizeof (cstr));
+ *end = '\0';
+ GNUNET_asprintf (
+ &res,
+ "%llu-%s",
+ (unsigned long long) (expiration.abs_time.abs_value_us / 1000LLU / 1000LLU),
+ cstr);
+ return res;
+}
diff --git a/src/backend/paivana-httpd_cookie.h b/src/backend/paivana-httpd_cookie.h
@@ -0,0 +1,76 @@
+/*
+ This file is part of GNUnet.
+ Copyright (C) 2026 Taler Systems SA
+
+ Paivana is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version
+ 3, or (at your option) any later version.
+
+ Paivana is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+ the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with Paivana; see the file COPYING. If not,
+ write to the Free Software Foundation, Inc., 51 Franklin
+ Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/**
+ * @author Christian Grothoff
+ * @file paivana-httpd_cookie.h
+ *
+ * @brief cookie generation and verification logic
+ */
+#ifndef PAIVANA_HTTPD_COOKIE_H
+#define PAIVANA_HTTPD_COOKIE_H
+
+#include <gnunet/gnunet_util_lib.h>
+
+/**
+ * Secret for the cookie generation.
+ */
+extern struct GNUNET_HashCode paivana_secret;
+
+
+/**
+ * Nonce generated client-side in Paivana protocol.
+ */
+struct PAIVANA_Nonce
+{
+ /**
+ * Some 128-bit value.
+ */
+ uint32_t val[4];
+};
+
+
+/**
+ * Check if the given cookie currently grants access.
+ *
+ * @param cookie the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @return true if the cookie is OK
+ */
+bool
+PAIVANA_HTTPD_check_cookie (const char *cookie,
+ size_t ca_len,
+ const void *ca);
+
+/**
+ * Compute access cookie hash for the given @a expiration and @a ca.
+ *
+ * @param expiration expiration time of the cookie
+ * @param ca_len number of bytes in @a ca
+ * @param ca client address
+ * @param[out] c set to the cookie hash
+ */
+char *
+PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp expiration,
+ size_t ca_len,
+ const void *ca);
+
+#endif
diff --git a/src/backend/paivana-httpd_pay.c b/src/backend/paivana-httpd_pay.c
@@ -0,0 +1,335 @@
+/*
+ This file is part of GNUnet.
+ Copyright (C) 2026 Taler Systems SA
+
+ Paivana is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version
+ 3, or (at your option) any later version.
+
+ Paivana is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+ the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with Paivana; see the file COPYING. If not,
+ write to the Free Software Foundation, Inc., 51 Franklin
+ Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/**
+ * @author Christian Grothoff
+ * @file paivana-httpd_pay.c
+ *
+ * @brief payment processing logic
+ */
+#include <gnunet/gnunet_util_lib.h>
+#include <microhttpd.h>
+#include <taler/taler_mhd_lib.h>
+#include <taler/taler_error_codes.h>
+#include "paivana-httpd_cookie.h"
+#include "paivana-httpd_pay.h"
+
+struct PayRequest;
+#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct PayRequest
+#include "taler/merchant/get-private-orders-ORDER_ID.h"
+
+
+/**
+ * Handle for processing actual payment.
+ */
+struct PayRequest
+{
+
+ /**
+ * Kept in a DLL while suspended.
+ */
+ struct PayRequest *next;
+
+ /**
+ * Kept in a DLL while suspended.
+ */
+ struct PayRequest *prev;
+
+ /**
+ * Connection we are handling.
+ */
+ struct MHD_Connection *connection;
+
+ /**
+ * Buffer for TALER_MHD_parse_post_json.
+ */
+ void *buffer;
+
+ /**
+ * Uploaded JSON body, NULL if none yet.
+ */
+ json_t *body;
+
+ /**
+ * Handle for our request to the merchant backend.
+ */
+ struct TALER_MERCHANT_GetPrivateOrderHandle *co;
+
+ /**
+ * Response to return.
+ */
+ struct MHD_Response *response;
+
+ /**
+ * ID of the order the client claims to have paid.
+ */
+ const char *order_id;
+
+ /**
+ * Website the order is supposed to have paid for.
+ */
+ const char *website;
+
+ /**
+ * Client-side nonce.
+ */
+ struct PAIVANA_Nonce nonce;
+
+ /**
+ *
+ */
+ struct GNUNET_TIME_Timestamp cur_time;
+
+ /**
+ * HTTP status to return in combination with @e resp to the client.
+ */
+ unsigned int response_status;
+
+};
+
+
+/**
+ * Head of DLL of suspended requests.
+ */
+static struct PayRequest *ph_head;
+
+/**
+ * Tail of DLL of suspended requests.
+ */
+static struct PayRequest *ph_tail;
+
+
+void
+PAIVANA_HTTPD_payment_shutdown ()
+{
+ while (NULL != ph_head)
+ {
+ struct PayRequest *ph = ph_head;
+
+ GNUNET_CONTAINER_DLL_remove (ph_head,
+ ph_tail,
+ ph);
+ MHD_resume_connection (ph->connection);
+ }
+}
+
+
+struct PayRequest *
+PAIVANA_HTTPD_payment_create (struct MHD_Connection *connection)
+{
+ struct PayRequest *ph;
+
+ ph = GNUNET_new (struct PayRequest);
+ ph->connection = connection;
+ return ph;
+}
+
+
+/**
+ * Handle response from the GET /private/orders/$ORDER_ID request.
+ *
+ * @param ph the payment request we are processing
+ * @param osr response details
+ */
+static void
+order_status_cb (struct PayRequest *ph,
+ const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
+{
+ ph->co = NULL;
+ switch (osr->hr.http_status)
+ {
+ case MHD_HTTP_OK:
+ if (TALER_MERCHANT_OSC_PAID != osr->details.ok.status)
+ {
+ GNUNET_break_op (0);
+ ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_PAYMENT_MISSING,
+ ph->order_id);
+ ph->response_status = MHD_HTTP_BAD_REQUEST;
+ }
+ else
+ {
+ const union MHD_ConnectionInfo *ci;
+ const struct sockaddr *ca;
+ socklen_t ca_len;
+ // FIXME: relationship of expiration to ph->cur_time?
+ struct GNUNET_TIME_Timestamp expiration;
+ char *cookie;
+ struct MHD_Response *resp;
+
+ // FIXME: de-duplicate with logic in paivana-httpd.c,
+ // and also support getting client address from HTTP
+ // headers instead (in case of reverse proxy).
+ ci = MHD_get_connection_info (ph->connection,
+ MHD_CONNECTION_INFO_CLIENT_ADDRESS);
+ GNUNET_assert (NULL != ci);
+ ca = ci->client_addr;
+ switch (ca->sa_family)
+ {
+ case AF_INET:
+ ca_len = sizeof (struct sockaddr_in);
+ break;
+ case AF_INET6:
+ ca_len = sizeof (struct sockaddr_in6);
+ break;
+ default:
+ GNUNET_break (0);
+ ca_len = 0;
+ break;
+ }
+ // FIXME: include website + nonce + cur_time somehow!!
+ // => TALER_EC_PAIVANA_WRONG_ORDER with 409!
+ cookie = PAIVANA_HTTPD_compute_cookie (expiration,
+ ca_len,
+ ca);
+ resp = MHD_create_response_from_buffer (0,
+ NULL,
+ MHD_RESPMEM_PERSISTENT);
+ GNUNET_assert (MHD_YES ==
+ MHD_add_response_header (resp,
+ MHD_HTTP_HEADER_SET_COOKIE,
+ cookie));
+ GNUNET_assert (MHD_YES ==
+ MHD_add_response_header (resp,
+ MHD_HTTP_HEADER_LOCATION,
+ ph->website));
+ GNUNET_free (cookie);
+ TALER_MHD_add_global_headers (resp,
+ false);
+ ph->response = resp;
+ ph->response_status = MHD_HTTP_SEE_OTHER;
+ }
+ break;
+ case MHD_HTTP_FORBIDDEN:
+ ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_REFUSED,
+ NULL);
+ ph->response_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ break;
+ case MHD_HTTP_NOT_FOUND:
+ ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_ORDER_UNKNOWN,
+ ph->order_id);
+ ph->response_status = MHD_HTTP_NOT_FOUND;
+ break;
+ default:
+ {
+ char code[20];
+
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Unexpected status code %u from backend\n",
+ osr->hr.http_status);
+ GNUNET_snprintf (code,
+ sizeof (code),
+ "%u",
+ osr->hr.http_status);
+ ph->response = TALER_MHD_make_error (TALER_EC_PAIVANA_BACKEND_ERROR,
+ code);
+ ph->response_status = MHD_HTTP_BAD_GATEWAY;
+ }
+ break;
+ }
+ GNUNET_CONTAINER_DLL_insert (ph_head,
+ ph_tail,
+ ph);
+ MHD_resume_connection (ph->connection);
+ TALER_MHD_daemon_trigger ();
+
+}
+
+
+enum MHD_Result
+PAIVANA_HTTPD_payment_handle (struct PayRequest *ph,
+ const char *upload_data,
+ size_t *upload_data_size)
+{
+ if (NULL == ph->body)
+ {
+ enum GNUNET_GenericReturnValue ret;
+
+ ret = TALER_MHD_parse_post_json (ph->connection,
+ &ph->buffer,
+ upload_data,
+ upload_data_size,
+ &ph->body);
+ if (GNUNET_OK != ret)
+ return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
+ if (NULL == ph->body)
+ return MHD_YES;
+ }
+ if (NULL != ph->response)
+ {
+ return MHD_queue_response (ph->connection,
+ ph->response_status,
+ ph->response);
+ }
+ if (NULL == ph->order_id)
+ {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("order_id",
+ &ph->order_id),
+ GNUNET_JSON_spec_string ("website",
+ &ph->website),
+ GNUNET_JSON_spec_timestamp ("cur_time",
+ &ph->cur_time),
+ GNUNET_JSON_spec_fixed_auto ("nonce",
+ &ph->nonce),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue ret;
+
+ ret = TALER_MHD_parse_json_data (ph->connection,
+ ph->body,
+ spec);
+ if (GNUNET_YES != ret)
+ return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
+ }
+ GNUNET_assert (NULL == ph->co);
+ ph->co = TALER_MERCHANT_get_private_order_create (PH_ctx,
+ PH_merchant_base_url,
+ ph->order_id);
+ if (NULL == ph->co)
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (ph->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAIVANA_GET_ORDER_FAILED,
+ ph->order_id);
+ }
+ GNUNET_CONTAINER_DLL_insert (ph_head,
+ ph_tail,
+ ph);
+ MHD_suspend_connection (ph->connection);
+ GNUNET_assert (TALER_EC_NONE ==
+ TALER_MERCHANT_get_private_order_start (ph->co,
+ &order_status_cb,
+ ph));
+ return MHD_YES;
+}
+
+
+void
+PAIVANA_HTTPD_payment_destroy (struct PayRequest *ph)
+{
+ TALER_MHD_parse_post_cleanup_callback (ph->buffer);
+ if (NULL != ph->co)
+ TALER_MERCHANT_get_private_order_cancel (ph->co);
+ if (NULL != ph->response)
+ MHD_destroy_response (ph->response);
+ json_decref (ph->body);
+ GNUNET_free (ph);
+}
diff --git a/src/backend/paivana-httpd_pay.h b/src/backend/paivana-httpd_pay.h
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNUnet.
+ Copyright (C) 2026 Taler Systems SA
+
+ Paivana is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version
+ 3, or (at your option) any later version.
+
+ Paivana is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+ the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with Paivana; see the file COPYING. If not,
+ write to the Free Software Foundation, Inc., 51 Franklin
+ Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/**
+ * @author Christian Grothoff
+ * @file paivana-httpd_pay.h
+ *
+ * @brief payment processing logic
+ */
+#ifndef PAIVANA_HTTPD_PAY_H
+#define PAIVANA_HTTPD_PAY_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include <microhttpd.h>
+#include "paivana-httpd.h"
+
+/**
+ * Handle for processing actual payment.
+ */
+struct PayRequest;
+
+void
+PAIVANA_HTTPD_payment_shutdown (void);
+
+
+struct PayRequest *
+PAIVANA_HTTPD_payment_create (struct MHD_Connection *connection);
+
+
+enum MHD_Result
+PAIVANA_HTTPD_payment_handle (struct PayRequest *pr,
+ const char *upload_data,
+ size_t *upload_data_size);
+
+
+void
+PAIVANA_HTTPD_payment_destroy (struct PayRequest *ph);
+
+
+#endif
diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c
@@ -1057,7 +1057,7 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr,
GNUNET_asprintf (&curlurl,
"%s%s",
- target_server_base_url,
+ PH_target_server_base_url,
hr->url);
curl_easy_setopt (hr->curl,
CURLOPT_URL,
@@ -1067,7 +1067,7 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr,
curlurl);
GNUNET_free (curlurl);
- host_hdr = build_host_header (target_server_base_url);
+ host_hdr = build_host_header (PH_target_server_base_url);
PAIVANA_LOG_DEBUG ("Faking the host header, %s\n",
host_hdr);
hr->headers = curl_slist_append (hr->headers,