paivana

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

commit e90465d04d6ef2d32a7ea0d920a2cb8cbfdc7d4a
parent c935fd0f80fb5758f3d28f42fbc5d3d70950e3bd
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 16 Apr 2026 11:06:08 +0200

refacotor httpd logic

Diffstat:
Acontrib/Makefile.am | 7+++++++
Msrc/backend/Makefile.am | 1+
Msrc/backend/paivana-httpd.c | 1182+++----------------------------------------------------------------------------
Asrc/backend/paivana-httpd.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/paivana-httpd_reverse.c | 1193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/paivana-httpd_reverse.h | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1366 insertions(+), 1144 deletions(-)

diff --git a/contrib/Makefile.am b/contrib/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = . + +paywallpkgdatadir = $(datadir)/paivana/ +paywallpkgdata_DATA = \ + paywall.html + +EXTRA_DIST = $(paywallpkgdata_DATA) diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -11,6 +11,7 @@ bin_PROGRAMS = \ paivana_httpd_SOURCES = \ paivana-httpd.c \ + paivana-httpd_reverse.c paivana-httpd_reverse.h \ paivana_pd.c paivana_pd.h paivana_httpd_LDADD = \ $(LIBGCRYPT_LIBS) \ diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c @@ -30,230 +30,27 @@ #include <curl/curl.h> #include <gnunet/gnunet_util_lib.h> #include <taler/taler_mhd_lib.h> +#include "paivana-httpd.h" +#include "paivana-httpd_reverse.h" #include "paivana_pd.h" -#define PAIVANA_LOG_INFO(...) \ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, __VA_ARGS__) -#define PAIVANA_LOG_DEBUG(...) \ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, __VA_ARGS__) -#define PAIVANA_LOG_WARNING(...) \ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, __VA_ARGS__) -#define PAIVANA_LOG_ERROR(...) \ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, __VA_ARGS__) - -#define REQUEST_BUFFER_MAX (1024 * 1024) -#define UNIX_BACKLOG 500 - -/** - * Log curl error. - * - * @param level log level - * @param fun name of curl_easy-function that gave the error - * @param rc return code from curl - */ -#define LOG_CURL_EASY(level,fun,rc) \ - GNUNET_log (level, _ ("%s failed at %s:%d: `%s'\n"), fun, __FILE__, \ - __LINE__, \ - curl_easy_strerror (rc)) - -/* ******** Datastructures for HTTP handling ********** */ - - -/** - * State machine for HTTP requests (per request). - */ -enum RequestState -{ - /** - * Starting state. - */ - REQUEST_STATE_WITH_MHD = 0, - - /** - * We've started receiving upload data from MHD. - */ - REQUEST_STATE_CLIENT_UPLOAD_STARTED, - - /** - * Wa have started uploading data to the proxied service. - */ - REQUEST_STATE_PROXY_UPLOAD_STARTED, - - /** - * We're done with the upload from MHD. - */ - REQUEST_STATE_CLIENT_UPLOAD_DONE, - - /** - * We're done uploading data to the proxied service. - */ - REQUEST_STATE_PROXY_UPLOAD_DONE, - - /** - * We've finished uploading data via CURL and can now download. - */ - REQUEST_STATE_PROXY_DOWNLOAD_STARTED, - - /** - * We've finished receiving download data from cURL. - */ - REQUEST_STATE_PROXY_DOWNLOAD_DONE -}; - - -/** - * A header list - */ -struct HttpResponseHeader -{ - /** - * DLL - */ - struct HttpResponseHeader *next; - - /** - * DLL - */ - struct HttpResponseHeader *prev; - - /** - * Header type - */ - char *type; - - /** - * Header value - */ - char *value; -}; +/* *********************** Globals **************************** */ -/** - * A structure for socks requests - */ -struct HttpRequest +struct RequestContext { - - /** - * Kept in DLL. - */ - struct HttpRequest *prev; - - /** - * Kept in DLL. - */ - struct HttpRequest *next; - - /** - * MHD request that triggered us. - */ - struct MHD_Connection *con; - - /** - * Client socket read task - */ - struct GNUNET_SCHEDULER_Task *rtask; - - /** - * Client socket write task - */ - struct GNUNET_SCHEDULER_Task *wtask; - - /** - * Hold the response obtained by modifying the original one. - */ - struct MHD_Response *mod_response; - - /** - * MHD response object for this request. - */ - struct MHD_Response *response; - - /** - * The URL to fetch - */ - char *url; - - /** - * Handle to cURL - */ - CURL *curl; - - /** - * HTTP request headers for the curl request. - */ - struct curl_slist *headers; - - /** - * Headers from response - */ - struct HttpResponseHeader *header_head; - - /** - * Headers from response - */ - struct HttpResponseHeader *header_tail; - - /** - * Buffer we use for moving data between MHD and - * curl (in both directions). - */ - char *io_buf; - - /** - * Number of bytes already in the IO buffer. - */ - size_t io_len; - /** - * Number of bytes allocated for the IO buffer. + * Handle for request forwarding as reverse proxy. */ - unsigned int io_size; - - /** - * HTTP response code to give to MHD for the response. - */ - unsigned int response_code; - - /** - * Request processing state machine. - */ - enum RequestState state; - - /** - * Did we suspend MHD processing? - */ - enum GNUNET_GenericReturnValue suspended; + struct HttpRequest *hr; /** - * Did we pause CURL processing? + * We are past the paywall, forward to client. */ - int curl_paused; + bool do_forward; }; -/* *********************** Globals **************************** */ - -/** - * The cURL download task (curl multi API). - */ -static struct GNUNET_SCHEDULER_Task *curl_download_task; - -/** - * DLL of active HTTP requests. - */ -static struct HttpRequest *hr_head; - -/** - * DLL of active HTTP requests. - */ -static struct HttpRequest *hr_tail; - -/** - * The cURL multi handle - */ -static CURLM *curl_multi; - /** * Set to true if we started a daemon. */ @@ -265,11 +62,6 @@ static bool have_daemons; static struct MHD_Response *paywall; /** - * Response we return on cURL failures. - */ -static struct MHD_Response *curl_failure_response; - -/** * Our configuration. */ static const struct GNUNET_CONFIGURATION_Handle *cfg; @@ -284,11 +76,7 @@ static int no_check; */ static int global_ret; -/** - * Destination to which HTTP server we forward requests to. - * Of the format "http://servername:PORT" - */ -static char *target_server_base_url; +char *target_server_base_url; /** * Merchant backend base URL. @@ -306,7 +94,7 @@ static char *merchant_access_token; static struct GNUNET_HashCode paivana_secret; -/* ********************* Cookie handling ****************** */ +/* ********************* Paivana Cookie handling ****************** */ /** * Compute access cookie hash for the given @a expiration and @a ca. @@ -422,576 +210,6 @@ compute_cookie (struct GNUNET_TIME_Timestamp expiration, } -/* ********************* Global helpers ****************** */ - -/** - * Run MHD now, we have extra data ready for the callback. - */ -static void -run_mhd_now (void); - - -/* *************** HTTP handling with cURL ***************** */ - - -/** - * Transform _one_ CURL header (gotten from the request) into - * MHD format and put it into the response headers list; mostly - * copies the headers, but makes special adjustments based on - * control requests. - * - * @param buffer curl buffer with a single - * line of header data; not 0-terminated! - * @param size curl blocksize - * @param nmemb curl blocknumber - * @param cls our `struct HttpRequest *` - * @return size of processed bytes - */ -static size_t -curl_check_hdr (void *buffer, - size_t size, - size_t nmemb, - void *cls) -{ - struct HttpRequest *hr = cls; - struct HttpResponseHeader *header; - size_t bytes = size * nmemb; - char *ndup; - const char *hdr_type; - char *hdr_val; - char *tok; - - /* Raw line is not guaranteed to be null-terminated. */ - ndup = GNUNET_malloc (bytes + 1); - memcpy (ndup, - buffer, - bytes); - ndup[bytes] = '\0'; - hdr_type = strtok (ndup, ":"); - if (NULL == hdr_type) - { - GNUNET_free (ndup); - return bytes; - } - hdr_val = strtok (NULL, ""); - if (NULL == hdr_val) - { - GNUNET_free (ndup); - return bytes; - } - if (' ' == *hdr_val) - hdr_val++; - - /* MHD does not allow certain characters in values, - * remove those, plus those could alter strings matching. */ - if (NULL != (tok = strchr (hdr_val, '\n'))) - *tok = '\0'; - if (NULL != (tok = strchr (hdr_val, '\r'))) - *tok = '\0'; - if (NULL != (tok = strchr (hdr_val, '\t'))) - *tok = '\0'; - PAIVANA_LOG_DEBUG ("Parsed line: '%s: %s'\n", - hdr_type, - hdr_val); - /* Skip "Content-length:" header as it will be wrong, given - that we are man-in-the-middling the connection */ - if (0 == strcasecmp (hdr_type, - MHD_HTTP_HEADER_CONTENT_LENGTH)) - { - GNUNET_free (ndup); - return bytes; - } - /* Skip "Connection: Keep-Alive" header, it will be - done by MHD if possible */ - if ( (0 == strcasecmp (hdr_type, - MHD_HTTP_HEADER_CONNECTION)) && - (0 == strcasecmp (hdr_val, - "Keep-Alive")) ) - { - GNUNET_free (ndup); - return bytes; - } - if (0 != strlen (hdr_val)) /* Rely in MHD to set those */ - { - header = GNUNET_new (struct HttpResponseHeader); - header->type = GNUNET_strdup (hdr_type); - header->value = GNUNET_strdup (hdr_val); - GNUNET_CONTAINER_DLL_insert (hr->header_head, - hr->header_tail, - header); - } - GNUNET_free (ndup); - return bytes; -} - - -/** - * Create the MHD response with CURL's as starting base; - * mainly set the response code and parses the response into - * JSON, if it is such. - * - * @param hr pointer to where to store the new data. Despite - * its name, the struct contains response data as well. - * @return #GNUNET_OK if it succeeds. - */ -static enum GNUNET_GenericReturnValue -create_mhd_response_from_hr (struct HttpRequest *hr) -{ - long resp_code; - - if (NULL != hr->response) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Response already set!\n"); - return GNUNET_SYSERR; - } - GNUNET_break (CURLE_OK == - curl_easy_getinfo (hr->curl, - CURLINFO_RESPONSE_CODE, - &resp_code)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Creating MHD response with code %u\n", - (unsigned int) resp_code); - hr->response_code = resp_code; - if (GNUNET_YES == hr->suspended) - { - MHD_resume_connection (hr->con); - hr->suspended = GNUNET_NO; - } - run_mhd_now (); - return GNUNET_OK; -} - - -/** - * Handle response payload data from cURL. - * Copies it into our `io_buf` to make it available to MHD. - * - * @param ptr pointer to the data - * @param size number of blocks of data - * @param nmemb blocksize - * @param ctx our `struct HttpRequest *` - * @return number of bytes handled - */ -static size_t -curl_download_cb (void *ptr, - size_t size, - size_t nmemb, - void *ctx) -{ - struct HttpRequest *hr = ctx; - size_t total = size * nmemb; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Curl download proceeding\n"); - - if (REQUEST_STATE_PROXY_UPLOAD_STARTED == hr->state) - { - /* Web server started with response before we finished - the upload. In this case, current libcurl decides - to NOT complete the upload, so we should jump in the - state machine to process the download, dropping the - rest of the upload. This should only really happen - with uploads without "Expect: 100 Continue" and - Web servers responding with an error (i.e. upload - not allowed) */hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - GNUNET_log - (GNUNET_ERROR_TYPE_INFO, - "Stopping %u byte upload: we are already downloading...\n", - (unsigned int) hr->io_len); - hr->io_len = 0; - } - - if (REQUEST_STATE_PROXY_DOWNLOAD_STARTED != hr->state) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Download callback goes to sleep\n"); - hr->curl_paused = GNUNET_YES; - return CURL_WRITEFUNC_PAUSE; - } - GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == - hr->state); - if (hr->io_size - hr->io_len < total) - { - GNUNET_assert (total + hr->io_size >= total); - GNUNET_assert (hr->io_size * 2 + 1024 > hr->io_size); - GNUNET_array_grow (hr->io_buf, - hr->io_size, - GNUNET_MAX (total + hr->io_len, - hr->io_size * 2 + 1024)); - } - GNUNET_memcpy (&hr->io_buf[hr->io_len], - ptr, - total); - hr->io_len += total; - return total; -} - - -/** - * Ask cURL for the select() sets and schedule cURL operations. - */ -static void -curl_download_prepare (void); - - -/** - * cURL callback for uploaded (PUT/POST) data. - * Copies from our `io_buf` to make it available to cURL. - * - * @param buf where to write the data - * @param size number of bytes per member - * @param nmemb number of members available in @a buf - * @param cls our `struct HttpRequest` that generated the data - * @return number of bytes copied to @a buf - */ -static size_t -curl_upload_cb (void *buf, - size_t size, - size_t nmemb, - void *cls) -{ - struct HttpRequest *hr = cls; - size_t len = size * nmemb; - size_t to_copy; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Upload cb is working...\n"); - - if ( (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == hr->state) || - (REQUEST_STATE_PROXY_DOWNLOAD_DONE == hr->state) ) - { - GNUNET_log - (GNUNET_ERROR_TYPE_INFO, - "Upload cb aborts: we are already downloading...\n"); - return CURL_READFUNC_ABORT; - } - - if ( (0 == hr->io_len) && - (REQUEST_STATE_PROXY_UPLOAD_STARTED == hr->state) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Pausing CURL UPLOAD, need more data\n"); - return CURL_READFUNC_PAUSE; - } - - /** - * We got rescheduled because the download callback was asleep. - * FIXME: can this block be eliminated and the unpausing being - * moved in the last block where we return zero as well? - */ - if ( (0 == hr->io_len) && - (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == hr->state) ) - { - if (GNUNET_YES == hr->curl_paused) - { - hr->curl_paused = GNUNET_NO; - curl_easy_pause (hr->curl, - CURLPAUSE_CONT); - } - curl_download_prepare (); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Completed CURL UPLOAD\n"); - return 0; /* upload finished, can now download */ - } - to_copy = GNUNET_MIN (hr->io_len, - len); - GNUNET_memcpy (buf, - hr->io_buf, - to_copy); - /* shift remaining data back to the beginning of the buffer. */ - memmove (hr->io_buf, - &hr->io_buf[to_copy], - hr->io_len - to_copy); - hr->io_len -= to_copy; - if (0 == hr->io_len) - { - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Completed CURL UPLOAD\n"); - } - return to_copy; -} - - -/* ************** helper functions ************* */ - -/** - * Extract the hostname from a complete URL. - * - * @param url full fledged URL - * @return pointer to the 0-terminated hostname, to be freed - * by the caller. - */ -static char * -build_host_header (const char *url) -{ - #define MARKER "://" - - char *header; - char *end; - char *hostname; - char *dup = GNUNET_strdup (url); - - hostname = strstr (dup, - MARKER); - hostname += 3; - end = strchrnul (hostname, '/'); - *end = '\0'; - GNUNET_asprintf (&header, - "Host: %s", - hostname); - GNUNET_free (dup); - return header; -} - - -/* ************** main loop of cURL interaction ************* */ - - -/** - * Task that is run when we are ready to receive more data - * from curl - * - * @param cls closure - */ -static void -curl_task_download (void *cls); - - -/** - * Ask cURL for the select() sets and schedule cURL operations. - */ -static void -curl_download_prepare () -{ - CURLMcode mret; - fd_set rs; - fd_set ws; - fd_set es; - int max; - struct GNUNET_NETWORK_FDSet *grs; - struct GNUNET_NETWORK_FDSet *gws; - long to; - struct GNUNET_TIME_Relative rtime; - - if (NULL != curl_download_task) - { - GNUNET_SCHEDULER_cancel (curl_download_task); - curl_download_task = NULL; - } - max = -1; - FD_ZERO (&rs); - FD_ZERO (&ws); - FD_ZERO (&es); - if (CURLM_OK != (mret = curl_multi_fdset (curl_multi, - &rs, - &ws, - &es, - &max))) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "%s failed at %s:%d: `%s'\n", - "curl_multi_fdset", - __FILE__, - __LINE__, - curl_multi_strerror (mret)); - return; - } - to = -1; - GNUNET_break (CURLM_OK == - curl_multi_timeout (curl_multi, - &to)); - if (-1 == to) - rtime = GNUNET_TIME_UNIT_FOREVER_REL; - else - rtime = GNUNET_TIME_relative_multiply - (GNUNET_TIME_UNIT_MILLISECONDS, to); - if (-1 != max) - { - grs = GNUNET_NETWORK_fdset_create (); - gws = GNUNET_NETWORK_fdset_create (); - GNUNET_NETWORK_fdset_copy_native (grs, - &rs, - max + 1); - GNUNET_NETWORK_fdset_copy_native (gws, - &ws, - max + 1); - curl_download_task - = GNUNET_SCHEDULER_add_select ( - GNUNET_SCHEDULER_PRIORITY_DEFAULT, - rtime, - grs, gws, - &curl_task_download, - curl_multi); - GNUNET_NETWORK_fdset_destroy (gws); - GNUNET_NETWORK_fdset_destroy (grs); - } - else - { - curl_download_task = GNUNET_SCHEDULER_add_delayed - (rtime, - &curl_task_download, - curl_multi); - } -} - - -/** - * "Filter" function that translates MHD request headers to - * cURL's. - * - * @param cls our `struct HttpRequest` - * @param kind value kind - * @param key field key - * @param value field value - * @return #MHD_YES to continue to iterate - */ -static enum MHD_Result -con_val_iter (void *cls, - enum MHD_ValueKind kind, - const char *key, - const char *value) -{ - struct HttpRequest *hr = cls; - char *hdr; - char *new_value = NULL; - - (void) kind; - if (0 == strcmp (MHD_HTTP_HEADER_HOST, - key)) - { - /* We don't take the host header as given in the request. - * We'll instead put the proxied service's hostname in it*/ - return MHD_YES; - } - if ((0 == strcmp (MHD_HTTP_HEADER_CONTENT_LENGTH, - key))) - { - PAIVANA_LOG_INFO ( - "Do not set Content-Length for request\n"); - return MHD_YES; - } - GNUNET_asprintf (&hdr, - "%s: %s", - key, - value); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding header `%s' to HTTP request\n", - hdr); - hr->headers = curl_slist_append (hr->headers, - hdr); - GNUNET_free (hdr); - GNUNET_free (new_value); - return MHD_YES; -} - - -/** - * Task that is run when we are ready to receive - * more data from curl. - * - * @param cls closure, usually NULL. - */ -static void -curl_task_download (void *cls) -{ - int running; - int msgnum; - struct CURLMsg *msg; - CURLMcode mret; - struct HttpRequest *hr; - - (void) cls; - curl_download_task = NULL; - do - { - running = 0; - mret = curl_multi_perform (curl_multi, - &running); - while (NULL != (msg = curl_multi_info_read (curl_multi, - &msgnum))) - { - GNUNET_break - (CURLE_OK == curl_easy_getinfo - (msg->easy_handle, - CURLINFO_PRIVATE, - (char **) &hr)); - - if (NULL == hr) - { - GNUNET_break (0); - continue; - } - switch (msg->msg) - { - case CURLMSG_NONE: - /* documentation says this is not used */ - GNUNET_break (0); - break; - case CURLMSG_DONE: - switch (msg->data.result) - { - case CURLE_OK: - case CURLE_GOT_NOTHING: - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "CURL download completed.\n"); - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; - if (NULL == hr->response) - GNUNET_assert (GNUNET_OK == - create_mhd_response_from_hr (hr)); - break; - default: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Download curl failed: %s\n", - curl_easy_strerror (msg->data.result)); - /* FIXME: indicate error somehow? - * close MHD connection badly as well? */ - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; - if (GNUNET_YES == hr->suspended) - { - MHD_resume_connection (hr->con); - hr->suspended = GNUNET_NO; - } - run_mhd_now (); - break; - } - if (NULL == hr->response) - hr->response = curl_failure_response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Curl request for `%s' finished (got the response)\n", - hr->url); - run_mhd_now (); - break; - case CURLMSG_LAST: - /* documentation says this is not used */ - GNUNET_break (0); - break; - default: - /* unexpected status code */ - GNUNET_break (0); - break; - } - } - } while (mret == CURLM_CALL_MULTI_PERFORM); - if (CURLM_OK != mret) - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "%s failed at %s:%d: `%s'\n", - "curl_multi_perform", - __FILE__, - __LINE__, - curl_multi_strerror (mret)); - if (0 == running) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending cURL multi loop," - " no more events pending\n"); - return; /* nothing more in progress */ - } - curl_download_prepare (); -} - - /* *************** MHD response generation ***************** */ @@ -1029,20 +247,18 @@ create_response (void *cls, size_t *upload_data_size, void **con_cls) { - struct HttpRequest *hr = *con_cls; + struct RequestContext *rc = *con_cls; + struct HttpRequest *hr = rc->hr; (void) cls; - // FIXME: check if url is one that we reverse proxy! - - (void) url; - if (NULL == hr) { GNUNET_break (0); return MHD_NO; } + // FIXME: check if url is one that we reverse proxy! - if (REQUEST_STATE_WITH_MHD == hr->state) + if (! rc->do_forward) { const char *cookie; bool ok = (0 != no_check); @@ -1087,258 +303,18 @@ create_response (void *cls, } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Request ok!\n"); - hr->state = REQUEST_STATE_CLIENT_UPLOAD_STARTED; - /* TODO: hacks for 100 continue suppression would go here! */ - return MHD_YES; - } - - // FIXME: move vanilla reverse proxy logic to another file! - - /* continuing to process request */ - if (0 != *upload_data_size) - { - GNUNET_assert - (REQUEST_STATE_CLIENT_UPLOAD_STARTED == hr->state); - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Processing %u bytes UPLOAD\n", - (unsigned int) *upload_data_size); - - /* Grow the buffer if remaining space isn't enough. */ - if (hr->io_size - hr->io_len < *upload_data_size) - { - /* How can this assertion be false? */ - GNUNET_assert (hr->io_size * 2 + 1024 > hr->io_size); - /* This asserts that upload_data_size > 0, ? */ - GNUNET_assert (*upload_data_size + hr->io_len > hr->io_len); - - GNUNET_array_grow (hr->io_buf, - hr->io_size, - GNUNET_MAX - (hr->io_size * 2 + 1024, - *upload_data_size + hr->io_len)); - } - - /* Finally copy upload data. */ - GNUNET_memcpy (&hr->io_buf[hr->io_len], - upload_data, - *upload_data_size); - - hr->io_len += *upload_data_size; - *upload_data_size = 0; - + rc->do_forward = true; + /* TODO: hacks for 100 continue suppression should go here! */ return MHD_YES; } - /* Upload (*from the client*) finished or just a without-body - * request. */ - if (REQUEST_STATE_CLIENT_UPLOAD_STARTED == hr->state) - { - hr->state = REQUEST_STATE_CLIENT_UPLOAD_DONE; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Finished processing UPLOAD\n"); - } - - /* generate curl request to the proxied service. */ - if (NULL == hr->curl) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Generating curl request\n"); - hr->curl = curl_easy_init (); - if (NULL == hr->curl) - { - PAIVANA_LOG_ERROR ("Could not init the curl handle\n"); - return MHD_queue_response (con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - curl_failure_response); - } - - /* No need to check whether we're POSTing or PUTting. - * If not needed, one of the following values will be - * ignored.*/ - curl_easy_setopt (hr->curl, - CURLOPT_POSTFIELDSIZE, - hr->io_len); - curl_easy_setopt (hr->curl, - CURLOPT_INFILESIZE, - hr->io_len); - curl_easy_setopt (hr->curl, - CURLOPT_HEADERFUNCTION, - &curl_check_hdr); - curl_easy_setopt (hr->curl, - CURLOPT_HEADERDATA, - hr); - curl_easy_setopt (hr->curl, - CURLOPT_FOLLOWLOCATION, - 0); - curl_easy_setopt (hr->curl, - CURLOPT_CONNECTTIMEOUT, - 60L); - curl_easy_setopt (hr->curl, - CURLOPT_TIMEOUT, - 60L); - curl_easy_setopt (hr->curl, - CURLOPT_NOSIGNAL, - 1L); - curl_easy_setopt (hr->curl, - CURLOPT_PRIVATE, - hr); - curl_easy_setopt (hr->curl, - CURLOPT_VERBOSE, - 0); - - curl_easy_setopt (hr->curl, - CURLOPT_READFUNCTION, - &curl_upload_cb); - curl_easy_setopt (hr->curl, - CURLOPT_READDATA, - hr); - - curl_easy_setopt (hr->curl, - CURLOPT_WRITEFUNCTION, - &curl_download_cb); - curl_easy_setopt (hr->curl, - CURLOPT_WRITEDATA, - hr); - { - char *curlurl; - char *host_hdr; - - GNUNET_asprintf (&curlurl, - "%s%s", - target_server_base_url, - hr->url); - curl_easy_setopt (hr->curl, - CURLOPT_URL, - curlurl); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Forwarding request to: %s\n", - curlurl); - GNUNET_free (curlurl); - - host_hdr = build_host_header (target_server_base_url); - PAIVANA_LOG_DEBUG ("Faking the host header, %s\n", - host_hdr); - hr->headers = curl_slist_append (hr->headers, - host_hdr); - GNUNET_free (host_hdr); - } - - // FIXME: support PATCH, etc. - if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_PUT)) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Crafting a CURL PUT request\n"); - - curl_easy_setopt (hr->curl, - CURLOPT_UPLOAD, - 1L); - hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; - } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_POST)) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Crafting a CURL POST request\n"); - curl_easy_setopt (hr->curl, - CURLOPT_POST, - 1L); - curl_easy_setopt (hr->curl, - CURLOPT_VERBOSE, - 1L); - hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; - } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_HEAD)) - { - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - curl_easy_setopt (hr->curl, - CURLOPT_NOBODY, - 1L); - } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_OPTIONS)) - { - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - curl_easy_setopt (hr->curl, - CURLOPT_CUSTOMREQUEST, - "OPTIONS"); - } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_GET)) - { - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - curl_easy_setopt (hr->curl, - CURLOPT_HTTPGET, - 1L); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Unsupported HTTP method `%s'\n", - meth); - curl_easy_cleanup (hr->curl); - hr->curl = NULL; - return MHD_NO; - } - - if (CURLM_OK != - curl_multi_add_handle (curl_multi, - hr->curl)) - { - GNUNET_break (0); - curl_easy_cleanup (hr->curl); - hr->curl = NULL; - return MHD_NO; - } - - MHD_get_connection_values (con, - MHD_HEADER_KIND, - &con_val_iter, - hr); - - curl_easy_setopt (hr->curl, - CURLOPT_HTTPHEADER, - hr->headers); - curl_download_prepare (); - - return MHD_YES; - } - - if (REQUEST_STATE_PROXY_DOWNLOAD_DONE != hr->state) - { - GNUNET_assert (GNUNET_NO == hr->suspended); - MHD_suspend_connection (con); - hr->suspended = GNUNET_YES; - return MHD_YES; /* wait for curl */ - } - - GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_DONE == hr->state); - - hr->response - = MHD_create_response_from_buffer_copy (hr->io_len, - hr->io_buf); - for (struct HttpResponseHeader *header = hr->header_head; - NULL != header; - header = header->next) - { - const char *value = header->value; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding MHD response header %s->%s\n", - header->type, - value); - GNUNET_break (MHD_YES == - MHD_add_response_header (hr->response, - header->type, - value)); - } - run_mhd_now (); - - return MHD_queue_response (con, - hr->response_code, - hr->response); + return PAIVANA_HTTPD_reverse (hr, + con, + url, + meth, + ver, + upload_data, + upload_data_size); } @@ -1353,7 +329,7 @@ create_response (void *cls, * @param connection connection handle * @param con_cls value as set by the last call to * the MHD_AccessHandlerCallback, should be - * our `struct HttpRequest *` (set by `create_response()`) + * our `struct RequestContext *` (created in `mhd_log_callback()`) * @param toe reason for request termination (ignored) */ static void @@ -1362,59 +338,18 @@ mhd_completed_cb (void *cls, void **con_cls, enum MHD_RequestTerminationCode toe) { - struct HttpRequest *hr = *con_cls; - struct HttpResponseHeader *header; + struct RequestContext *rc = *con_cls; (void) cls; (void) connection; - if (NULL == hr) + if (NULL == rc) return; if (MHD_REQUEST_TERMINATED_COMPLETED_OK != toe) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "MHD encountered error handling request: %d\n", toe); - if (NULL != hr->curl) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resetting cURL handle\n"); - curl_multi_remove_handle (curl_multi, - hr->curl); - curl_easy_cleanup (hr->curl); - hr->curl = NULL; - hr->io_len = 0; - } - if (NULL != hr->headers) - { - curl_slist_free_all (hr->headers); - hr->headers = NULL; - } - if ( (NULL != hr->response) && - (curl_failure_response != hr->response) ) - /* Destroy non-error responses... (?) */ - MHD_destroy_response (hr->response); - - for (header = hr->header_head; - header != NULL; - header = hr->header_head) - { - GNUNET_CONTAINER_DLL_remove (hr->header_head, - hr->header_tail, - header); - GNUNET_free (header->type); - GNUNET_free (header->value); - GNUNET_free (header); - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Proxying of '%s' completely done\n", - hr->url); - - GNUNET_free (hr->url); - GNUNET_free (hr->io_buf); - GNUNET_CONTAINER_DLL_remove (hr_head, - hr_tail, - hr); - GNUNET_free (hr); + PAIVANA_HTTPD_reverse_cleanup (rc->hr); + GNUNET_free (rc); *con_cls = NULL; } @@ -1430,14 +365,14 @@ mhd_completed_cb (void *cls, * @param cls the HTTP server handle (a `struct MhdHttpList`) * @param url the URL that is being requested * @param connection MHD connection object for the request - * @return the `struct HttpRequest` that this @a connection is for + * @return the `struct RequestContext` that this @a connection is for */ static void * mhd_log_callback (void *cls, const char *url, struct MHD_Connection *connection) { - struct HttpRequest *hr; + struct RequestContext *rc; const union MHD_ConnectionInfo *ci; (void) cls; @@ -1451,24 +386,10 @@ mhd_log_callback (void *cls, GNUNET_break (0); return NULL; } - - hr = GNUNET_new (struct HttpRequest); - hr->con = connection; - hr->url = GNUNET_strdup (url); - GNUNET_CONTAINER_DLL_insert (hr_head, - hr_tail, - hr); - return hr; -} - - -/** - * Run MHD now, we have extra data ready for the callback. - */ -static void -run_mhd_now (void) -{ - TALER_MHD_daemon_trigger (); + rc = GNUNET_new (struct RequestContext); + rc->hr = PAIVANA_HTTPD_reverse_create (connection, + url); + return rc; } @@ -1487,28 +408,8 @@ do_shutdown (void *cls) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Shutting down...\n"); TALER_MHD_daemons_halt (); - /* MHD requires resuming before destroying the daemons */ - for (struct HttpRequest *hr = hr_head; - NULL != hr; - hr = hr->next) - { - if (GNUNET_YES == hr->suspended) - { - hr->suspended = GNUNET_NO; - MHD_resume_connection (hr->con); - } - } + PAIVANA_HTTPD_reverse_shutdown (); TALER_MHD_daemons_destroy (); - if (NULL != curl_multi) - { - curl_multi_cleanup (curl_multi); - curl_multi = NULL; - } - if (NULL != curl_download_task) - { - GNUNET_SCHEDULER_cancel (curl_download_task); - curl_download_task = NULL; - } GNUNET_free (target_server_base_url); } @@ -1628,22 +529,15 @@ run (void *cls, (void) args; (void) cfgfile; cfg = c; - if (0 != curl_global_init (CURL_GLOBAL_WIN32)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "cURL global init failed!\n"); - GNUNET_SCHEDULER_shutdown (); - return; - } + if (! load_paywall ()) { GNUNET_SCHEDULER_shutdown (); return; } - if (NULL == (curl_multi = curl_multi_init ())) + if (! PAIVANA_HTTPD_reverse_init ()) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to create cURL multi handle!\n"); + GNUNET_SCHEDULER_shutdown (); return; } diff --git a/src/backend/paivana-httpd.h b/src/backend/paivana-httpd.h @@ -0,0 +1,46 @@ +/* + 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.h + * + * @brief + */ +#ifndef PAIVANA_HTTPD_H +#define PAIVANA_HTTPD_H + +#define PAIVANA_LOG_INFO(...) \ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, __VA_ARGS__) +#define PAIVANA_LOG_DEBUG(...) \ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, __VA_ARGS__) +#define PAIVANA_LOG_WARNING(...) \ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, __VA_ARGS__) +#define PAIVANA_LOG_ERROR(...) \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, __VA_ARGS__) + +/** + * Destination to which HTTP server we forward requests to. + * Of the format "http://servername:PORT" + */ +extern char *target_server_base_url; + + +#endif diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c @@ -0,0 +1,1193 @@ +/* + This file is part of GNU Taler + Copyright (C) 2012-2014 GNUnet e.V. + Copyright (C) 2018, 2025, 2026 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_reverse.c + * @brief Reverse proxy logic that just forwards the request + */ +#include "platform.h" +#include <curl/curl.h> +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_mhd_lib.h> +#include "paivana-httpd_reverse.h" + +#define REQUEST_BUFFER_MAX (1024 * 1024) + + +/** + * Log curl error. + * + * @param level log level + * @param fun name of curl_easy-function that gave the error + * @param rc return code from curl + */ +#define LOG_CURL_EASY(level,fun,rc) \ + GNUNET_log (level, _ ("%s failed at %s:%d: `%s'\n"), fun, __FILE__, \ + __LINE__, \ + curl_easy_strerror (rc)) + + +/** + * State machine for HTTP requests (per request). + */ +enum RequestState +{ + /** + * We've started receiving upload data from MHD. + * Initial state. + */ + REQUEST_STATE_CLIENT_UPLOAD_STARTED, + + /** + * Wa have started uploading data to the proxied service. + */ + REQUEST_STATE_PROXY_UPLOAD_STARTED, + + /** + * We're done with the upload from MHD. + */ + REQUEST_STATE_CLIENT_UPLOAD_DONE, + + /** + * We're done uploading data to the proxied service. + */ + REQUEST_STATE_PROXY_UPLOAD_DONE, + + /** + * We've finished uploading data via CURL and can now download. + */ + REQUEST_STATE_PROXY_DOWNLOAD_STARTED, + + /** + * We've finished receiving download data from cURL. + */ + REQUEST_STATE_PROXY_DOWNLOAD_DONE +}; + + +/** + * A header list + */ +struct HttpResponseHeader +{ + /** + * DLL + */ + struct HttpResponseHeader *next; + + /** + * DLL + */ + struct HttpResponseHeader *prev; + + /** + * Header type + */ + char *type; + + /** + * Header value + */ + char *value; +}; + + +/** + * A structure for socks requests + */ +struct HttpRequest +{ + + /** + * Kept in DLL. + */ + struct HttpRequest *prev; + + /** + * Kept in DLL. + */ + struct HttpRequest *next; + + /** + * MHD request that triggered us. + */ + struct MHD_Connection *con; + + /** + * Client socket read task + */ + struct GNUNET_SCHEDULER_Task *rtask; + + /** + * Client socket write task + */ + struct GNUNET_SCHEDULER_Task *wtask; + + /** + * Hold the response obtained by modifying the original one. + */ + struct MHD_Response *mod_response; + + /** + * MHD response object for this request. + */ + struct MHD_Response *response; + + /** + * The URL to fetch + */ + char *url; + + /** + * Handle to cURL + */ + CURL *curl; + + /** + * HTTP request headers for the curl request. + */ + struct curl_slist *headers; + + /** + * Headers from response + */ + struct HttpResponseHeader *header_head; + + /** + * Headers from response + */ + struct HttpResponseHeader *header_tail; + + /** + * Buffer we use for moving data between MHD and + * curl (in both directions). + */ + char *io_buf; + + /** + * Number of bytes already in the IO buffer. + */ + size_t io_len; + + /** + * Number of bytes allocated for the IO buffer. + */ + unsigned int io_size; + + /** + * HTTP response code to give to MHD for the response. + */ + unsigned int response_code; + + /** + * Request processing state machine. + */ + enum RequestState state; + + /** + * Did we suspend MHD processing? + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Did we pause CURL processing? + */ + int curl_paused; +}; + + +/** + * DLL of active HTTP requests. + */ +static struct HttpRequest *hr_head; + +/** + * DLL of active HTTP requests. + */ +static struct HttpRequest *hr_tail; + +/** + * Response we return on cURL failures. + */ +static struct MHD_Response *curl_failure_response; + +/** + * The cURL multi handle + */ +static CURLM *curl_multi; + +/** + * The cURL download task (curl multi API). + */ +static struct GNUNET_SCHEDULER_Task *curl_download_task; + + +bool +PAIVANA_HTTPD_reverse_init (void) +{ + if (0 != curl_global_init (CURL_GLOBAL_WIN32)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "cURL global init failed!\n"); + return false; + } + if (NULL == (curl_multi = curl_multi_init ())) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to create cURL multi handle!\n"); + return false; + } + return true; +} + + +void +PAIVANA_HTTPD_reverse_shutdown (void) +{ + for (struct HttpRequest *hr = hr_head; + NULL != hr; + hr = hr->next) + { + if (GNUNET_YES == hr->suspended) + { + hr->suspended = GNUNET_NO; + MHD_resume_connection (hr->con); + } + } + if (NULL != curl_multi) + { + curl_multi_cleanup (curl_multi); + curl_multi = NULL; + } + if (NULL != curl_download_task) + { + GNUNET_SCHEDULER_cancel (curl_download_task); + curl_download_task = NULL; + } +} + + +/* *************** HTTP handling with cURL ***************** */ + + +/** + * Transform _one_ CURL header (gotten from the request) into + * MHD format and put it into the response headers list; mostly + * copies the headers, but makes special adjustments based on + * control requests. + * + * @param buffer curl buffer with a single + * line of header data; not 0-terminated! + * @param size curl blocksize + * @param nmemb curl blocknumber + * @param cls our `struct HttpRequest *` + * @return size of processed bytes + */ +static size_t +curl_check_hdr (void *buffer, + size_t size, + size_t nmemb, + void *cls) +{ + struct HttpRequest *hr = cls; + struct HttpResponseHeader *header; + size_t bytes = size * nmemb; + char *ndup; + const char *hdr_type; + char *hdr_val; + char *tok; + + /* Raw line is not guaranteed to be null-terminated. */ + ndup = GNUNET_malloc (bytes + 1); + memcpy (ndup, + buffer, + bytes); + ndup[bytes] = '\0'; + hdr_type = strtok (ndup, ":"); + if (NULL == hdr_type) + { + GNUNET_free (ndup); + return bytes; + } + hdr_val = strtok (NULL, ""); + if (NULL == hdr_val) + { + GNUNET_free (ndup); + return bytes; + } + if (' ' == *hdr_val) + hdr_val++; + + /* MHD does not allow certain characters in values, + * remove those, plus those could alter strings matching. */ + if (NULL != (tok = strchr (hdr_val, '\n'))) + *tok = '\0'; + if (NULL != (tok = strchr (hdr_val, '\r'))) + *tok = '\0'; + if (NULL != (tok = strchr (hdr_val, '\t'))) + *tok = '\0'; + PAIVANA_LOG_DEBUG ("Parsed line: '%s: %s'\n", + hdr_type, + hdr_val); + /* Skip "Content-length:" header as it will be wrong, given + that we are man-in-the-middling the connection */ + if (0 == strcasecmp (hdr_type, + MHD_HTTP_HEADER_CONTENT_LENGTH)) + { + GNUNET_free (ndup); + return bytes; + } + /* Skip "Connection: Keep-Alive" header, it will be + done by MHD if possible */ + if ( (0 == strcasecmp (hdr_type, + MHD_HTTP_HEADER_CONNECTION)) && + (0 == strcasecmp (hdr_val, + "Keep-Alive")) ) + { + GNUNET_free (ndup); + return bytes; + } + if (0 != strlen (hdr_val)) /* Rely in MHD to set those */ + { + header = GNUNET_new (struct HttpResponseHeader); + header->type = GNUNET_strdup (hdr_type); + header->value = GNUNET_strdup (hdr_val); + GNUNET_CONTAINER_DLL_insert (hr->header_head, + hr->header_tail, + header); + } + GNUNET_free (ndup); + return bytes; +} + + +/** + * Create the MHD response with CURL's as starting base; + * mainly set the response code and parses the response into + * JSON, if it is such. + * + * @param hr pointer to where to store the new data. Despite + * its name, the struct contains response data as well. + * @return #GNUNET_OK if it succeeds. + */ +static enum GNUNET_GenericReturnValue +create_mhd_response_from_hr (struct HttpRequest *hr) +{ + long resp_code; + + if (NULL != hr->response) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Response already set!\n"); + return GNUNET_SYSERR; + } + GNUNET_break (CURLE_OK == + curl_easy_getinfo (hr->curl, + CURLINFO_RESPONSE_CODE, + &resp_code)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Creating MHD response with code %u\n", + (unsigned int) resp_code); + hr->response_code = resp_code; + if (GNUNET_YES == hr->suspended) + { + MHD_resume_connection (hr->con); + hr->suspended = GNUNET_NO; + } + TALER_MHD_daemon_trigger (); + return GNUNET_OK; +} + + +/** + * Handle response payload data from cURL. + * Copies it into our `io_buf` to make it available to MHD. + * + * @param ptr pointer to the data + * @param size number of blocks of data + * @param nmemb blocksize + * @param ctx our `struct HttpRequest *` + * @return number of bytes handled + */ +static size_t +curl_download_cb (void *ptr, + size_t size, + size_t nmemb, + void *ctx) +{ + struct HttpRequest *hr = ctx; + size_t total = size * nmemb; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Curl download proceeding\n"); + + if (REQUEST_STATE_PROXY_UPLOAD_STARTED == hr->state) + { + /* Web server started with response before we finished + the upload. In this case, current libcurl decides + to NOT complete the upload, so we should jump in the + state machine to process the download, dropping the + rest of the upload. This should only really happen + with uploads without "Expect: 100 Continue" and + Web servers responding with an error (i.e. upload + not allowed) */hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + GNUNET_log + (GNUNET_ERROR_TYPE_INFO, + "Stopping %u byte upload: we are already downloading...\n", + (unsigned int) hr->io_len); + hr->io_len = 0; + } + + if (REQUEST_STATE_PROXY_DOWNLOAD_STARTED != hr->state) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Download callback goes to sleep\n"); + hr->curl_paused = GNUNET_YES; + return CURL_WRITEFUNC_PAUSE; + } + GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == + hr->state); + if (hr->io_size - hr->io_len < total) + { + GNUNET_assert (total + hr->io_size >= total); + GNUNET_assert (hr->io_size * 2 + 1024 > hr->io_size); + GNUNET_array_grow (hr->io_buf, + hr->io_size, + GNUNET_MAX (total + hr->io_len, + hr->io_size * 2 + 1024)); + } + GNUNET_memcpy (&hr->io_buf[hr->io_len], + ptr, + total); + hr->io_len += total; + return total; +} + + +/** + * Ask cURL for the select() sets and schedule cURL operations. + */ +static void +curl_download_prepare (void); + + +/** + * cURL callback for uploaded (PUT/POST) data. + * Copies from our `io_buf` to make it available to cURL. + * + * @param buf where to write the data + * @param size number of bytes per member + * @param nmemb number of members available in @a buf + * @param cls our `struct HttpRequest` that generated the data + * @return number of bytes copied to @a buf + */ +static size_t +curl_upload_cb (void *buf, + size_t size, + size_t nmemb, + void *cls) +{ + struct HttpRequest *hr = cls; + size_t len = size * nmemb; + size_t to_copy; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Upload cb is working...\n"); + + if ( (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == hr->state) || + (REQUEST_STATE_PROXY_DOWNLOAD_DONE == hr->state) ) + { + GNUNET_log + (GNUNET_ERROR_TYPE_INFO, + "Upload cb aborts: we are already downloading...\n"); + return CURL_READFUNC_ABORT; + } + + if ( (0 == hr->io_len) && + (REQUEST_STATE_PROXY_UPLOAD_STARTED == hr->state) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Pausing CURL UPLOAD, need more data\n"); + return CURL_READFUNC_PAUSE; + } + + /** + * We got rescheduled because the download callback was asleep. + * FIXME: can this block be eliminated and the unpausing being + * moved in the last block where we return zero as well? + */ + if ( (0 == hr->io_len) && + (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == hr->state) ) + { + if (GNUNET_YES == hr->curl_paused) + { + hr->curl_paused = GNUNET_NO; + curl_easy_pause (hr->curl, + CURLPAUSE_CONT); + } + curl_download_prepare (); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Completed CURL UPLOAD\n"); + return 0; /* upload finished, can now download */ + } + to_copy = GNUNET_MIN (hr->io_len, + len); + GNUNET_memcpy (buf, + hr->io_buf, + to_copy); + /* shift remaining data back to the beginning of the buffer. */ + memmove (hr->io_buf, + &hr->io_buf[to_copy], + hr->io_len - to_copy); + hr->io_len -= to_copy; + if (0 == hr->io_len) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Completed CURL UPLOAD\n"); + } + return to_copy; +} + + +/* ************** helper functions ************* */ + +/** + * Extract the hostname from a complete URL. + * + * @param url full fledged URL + * @return pointer to the 0-terminated hostname, to be freed + * by the caller. + */ +static char * +build_host_header (const char *url) +{ + #define MARKER "://" + + char *header; + char *end; + char *hostname; + char *dup = GNUNET_strdup (url); + + hostname = strstr (dup, + MARKER); + hostname += 3; + end = strchrnul (hostname, '/'); + *end = '\0'; + GNUNET_asprintf (&header, + "Host: %s", + hostname); + GNUNET_free (dup); + return header; +} + + +/* ************** main loop of cURL interaction ************* */ + + +/** + * Task that is run when we are ready to receive more data + * from curl + * + * @param cls closure + */ +static void +curl_task_download (void *cls); + + +/** + * Ask cURL for the select() sets and schedule cURL operations. + */ +static void +curl_download_prepare () +{ + CURLMcode mret; + fd_set rs; + fd_set ws; + fd_set es; + int max; + struct GNUNET_NETWORK_FDSet *grs; + struct GNUNET_NETWORK_FDSet *gws; + long to; + struct GNUNET_TIME_Relative rtime; + + if (NULL != curl_download_task) + { + GNUNET_SCHEDULER_cancel (curl_download_task); + curl_download_task = NULL; + } + max = -1; + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + if (CURLM_OK != (mret = curl_multi_fdset (curl_multi, + &rs, + &ws, + &es, + &max))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s failed at %s:%d: `%s'\n", + "curl_multi_fdset", + __FILE__, + __LINE__, + curl_multi_strerror (mret)); + return; + } + to = -1; + GNUNET_break (CURLM_OK == + curl_multi_timeout (curl_multi, + &to)); + if (-1 == to) + rtime = GNUNET_TIME_UNIT_FOREVER_REL; + else + rtime = GNUNET_TIME_relative_multiply + (GNUNET_TIME_UNIT_MILLISECONDS, to); + if (-1 != max) + { + grs = GNUNET_NETWORK_fdset_create (); + gws = GNUNET_NETWORK_fdset_create (); + GNUNET_NETWORK_fdset_copy_native (grs, + &rs, + max + 1); + GNUNET_NETWORK_fdset_copy_native (gws, + &ws, + max + 1); + curl_download_task + = GNUNET_SCHEDULER_add_select ( + GNUNET_SCHEDULER_PRIORITY_DEFAULT, + rtime, + grs, gws, + &curl_task_download, + curl_multi); + GNUNET_NETWORK_fdset_destroy (gws); + GNUNET_NETWORK_fdset_destroy (grs); + } + else + { + curl_download_task = GNUNET_SCHEDULER_add_delayed (rtime, + &curl_task_download, + curl_multi); + } +} + + +/** + * "Filter" function that translates MHD request headers to + * cURL's. + * + * @param cls our `struct HttpRequest` + * @param kind value kind + * @param key field key + * @param value field value + * @return #MHD_YES to continue to iterate + */ +static enum MHD_Result +con_val_iter (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *value) +{ + struct HttpRequest *hr = cls; + char *hdr; + char *new_value = NULL; + + (void) kind; + if (0 == strcmp (MHD_HTTP_HEADER_HOST, + key)) + { + /* We don't take the host header as given in the request. + * We'll instead put the proxied service's hostname in it*/ + return MHD_YES; + } + if ((0 == strcmp (MHD_HTTP_HEADER_CONTENT_LENGTH, + key))) + { + PAIVANA_LOG_INFO ( + "Do not set Content-Length for request\n"); + return MHD_YES; + } + GNUNET_asprintf (&hdr, + "%s: %s", + key, + value); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding header `%s' to HTTP request\n", + hdr); + hr->headers = curl_slist_append (hr->headers, + hdr); + GNUNET_free (hdr); + GNUNET_free (new_value); + return MHD_YES; +} + + +/** + * Task that is run when we are ready to receive + * more data from curl. + * + * @param cls closure, usually NULL. + */ +static void +curl_task_download (void *cls) +{ + int running; + int msgnum; + struct CURLMsg *msg; + CURLMcode mret; + struct HttpRequest *hr; + + (void) cls; + curl_download_task = NULL; + do + { + running = 0; + mret = curl_multi_perform (curl_multi, + &running); + while (NULL != (msg = curl_multi_info_read (curl_multi, + &msgnum))) + { + GNUNET_break + (CURLE_OK == curl_easy_getinfo + (msg->easy_handle, + CURLINFO_PRIVATE, + (char **) &hr)); + + if (NULL == hr) + { + GNUNET_break (0); + continue; + } + switch (msg->msg) + { + case CURLMSG_NONE: + /* documentation says this is not used */ + GNUNET_break (0); + break; + case CURLMSG_DONE: + switch (msg->data.result) + { + case CURLE_OK: + case CURLE_GOT_NOTHING: + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "CURL download completed.\n"); + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; + if (NULL == hr->response) + GNUNET_assert (GNUNET_OK == + create_mhd_response_from_hr (hr)); + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Download curl failed: %s\n", + curl_easy_strerror (msg->data.result)); + /* FIXME: indicate error somehow? + * close MHD connection badly as well? */ + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; + if (GNUNET_YES == hr->suspended) + { + MHD_resume_connection (hr->con); + hr->suspended = GNUNET_NO; + } + TALER_MHD_daemon_trigger (); + break; + } + if (NULL == hr->response) + hr->response = curl_failure_response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Curl request for `%s' finished (got the response)\n", + hr->url); + TALER_MHD_daemon_trigger (); + break; + case CURLMSG_LAST: + /* documentation says this is not used */ + GNUNET_break (0); + break; + default: + /* unexpected status code */ + GNUNET_break (0); + break; + } + } + } while (mret == CURLM_CALL_MULTI_PERFORM); + if (CURLM_OK != mret) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "%s failed at %s:%d: `%s'\n", + "curl_multi_perform", + __FILE__, + __LINE__, + curl_multi_strerror (mret)); + if (0 == running) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending cURL multi loop," + " no more events pending\n"); + return; /* nothing more in progress */ + } + curl_download_prepare (); +} + + +struct HttpRequest * +PAIVANA_HTTPD_reverse_create (struct MHD_Connection *connection, + const char *url) +{ + struct HttpRequest *hr; + + hr = GNUNET_new (struct HttpRequest); + hr->con = connection; + hr->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (hr_head, + hr_tail, + hr); + return hr; +} + + +void +PAIVANA_HTTPD_reverse_cleanup (struct HttpRequest *hr) +{ + struct HttpResponseHeader *header; + + if (NULL != hr->curl) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resetting cURL handle\n"); + curl_multi_remove_handle (curl_multi, + hr->curl); + curl_easy_cleanup (hr->curl); + hr->curl = NULL; + hr->io_len = 0; + } + if (NULL != hr->headers) + { + curl_slist_free_all (hr->headers); + hr->headers = NULL; + } + if ( (NULL != hr->response) && + (curl_failure_response != hr->response) ) + /* Destroy non-error responses... (?) */ + MHD_destroy_response (hr->response); + + for (header = hr->header_head; + header != NULL; + header = hr->header_head) + { + GNUNET_CONTAINER_DLL_remove (hr->header_head, + hr->header_tail, + header); + GNUNET_free (header->type); + GNUNET_free (header->value); + GNUNET_free (header); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Proxying of '%s' completely done\n", + hr->url); + + GNUNET_free (hr->url); + GNUNET_free (hr->io_buf); + GNUNET_CONTAINER_DLL_remove (hr_head, + hr_tail, + hr); + GNUNET_free (hr); +} + + +/** + * Main MHD callback for reverse proxy. + * + * @param hr the HTTP request context + * @param con MHD connection handle + * @param url the url in the request + * @param meth the HTTP method used ("GET", "PUT", etc.) + * @param ver the HTTP version string (i.e. "HTTP/1.1") + * @param upload_data the data being uploaded (excluding HEADERS, + * for a POST that fits into memory and that is encoded + * with a supported encoding, the POST data will NOT be + * given in upload_data and is instead available as + * part of MHD_get_connection_values; very large POST + * data *will* be made available incrementally in + * upload_data) + * @param upload_data_size set initially to the size of the + * @a upload_data provided; the method must update this + * value to the number of bytes NOT processed; + * @return #MHD_YES if the connection was handled successfully, + * #MHD_NO if the socket must be closed due to a serious + * error while handling the request + */ +enum MHD_Result +PAIVANA_HTTPD_reverse (struct HttpRequest *hr, + struct MHD_Connection *con, + const char *url, + const char *meth, + const char *ver, + const char *upload_data, + size_t *upload_data_size) +{ + /* FIXME: make state machine more explicit by + switching on hr->state here! */ + if (0 != *upload_data_size) + { + GNUNET_assert + (REQUEST_STATE_CLIENT_UPLOAD_STARTED == hr->state); + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Processing %u bytes UPLOAD\n", + (unsigned int) *upload_data_size); + + /* Grow the buffer if remaining space isn't enough. */ + if (hr->io_size - hr->io_len < *upload_data_size) + { + /* How can this assertion be false? */ + GNUNET_assert (hr->io_size * 2 + 1024 > hr->io_size); + /* This asserts that upload_data_size > 0, ? */ + GNUNET_assert (*upload_data_size + hr->io_len > hr->io_len); + + GNUNET_array_grow (hr->io_buf, + hr->io_size, + GNUNET_MAX + (hr->io_size * 2 + 1024, + *upload_data_size + hr->io_len)); + } + + /* Finally copy upload data. */ + GNUNET_memcpy (&hr->io_buf[hr->io_len], + upload_data, + *upload_data_size); + + hr->io_len += *upload_data_size; + *upload_data_size = 0; + + return MHD_YES; + } + + /* Upload (*from the client*) finished or just a without-body + * request. */ + if (REQUEST_STATE_CLIENT_UPLOAD_STARTED == hr->state) + { + hr->state = REQUEST_STATE_CLIENT_UPLOAD_DONE; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Finished processing UPLOAD\n"); + } + + /* generate curl request to the proxied service. */ + if (NULL == hr->curl) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Generating curl request\n"); + hr->curl = curl_easy_init (); + if (NULL == hr->curl) + { + PAIVANA_LOG_ERROR ("Could not init the curl handle\n"); + return MHD_queue_response (con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + curl_failure_response); + } + + /* No need to check whether we're POSTing or PUTting. + * If not needed, one of the following values will be + * ignored.*/ + curl_easy_setopt (hr->curl, + CURLOPT_POSTFIELDSIZE, + hr->io_len); + curl_easy_setopt (hr->curl, + CURLOPT_INFILESIZE, + hr->io_len); + curl_easy_setopt (hr->curl, + CURLOPT_HEADERFUNCTION, + &curl_check_hdr); + curl_easy_setopt (hr->curl, + CURLOPT_HEADERDATA, + hr); + curl_easy_setopt (hr->curl, + CURLOPT_FOLLOWLOCATION, + 0); + curl_easy_setopt (hr->curl, + CURLOPT_CONNECTTIMEOUT, + 60L); + curl_easy_setopt (hr->curl, + CURLOPT_TIMEOUT, + 60L); + curl_easy_setopt (hr->curl, + CURLOPT_NOSIGNAL, + 1L); + curl_easy_setopt (hr->curl, + CURLOPT_PRIVATE, + hr); + curl_easy_setopt (hr->curl, + CURLOPT_VERBOSE, + 0); + + curl_easy_setopt (hr->curl, + CURLOPT_READFUNCTION, + &curl_upload_cb); + curl_easy_setopt (hr->curl, + CURLOPT_READDATA, + hr); + + curl_easy_setopt (hr->curl, + CURLOPT_WRITEFUNCTION, + &curl_download_cb); + curl_easy_setopt (hr->curl, + CURLOPT_WRITEDATA, + hr); + { + char *curlurl; + char *host_hdr; + + GNUNET_asprintf (&curlurl, + "%s%s", + target_server_base_url, + hr->url); + curl_easy_setopt (hr->curl, + CURLOPT_URL, + curlurl); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Forwarding request to: %s\n", + curlurl); + GNUNET_free (curlurl); + + host_hdr = build_host_header (target_server_base_url); + PAIVANA_LOG_DEBUG ("Faking the host header, %s\n", + host_hdr); + hr->headers = curl_slist_append (hr->headers, + host_hdr); + GNUNET_free (host_hdr); + } + + // FIXME: support PATCH, etc. + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_PUT)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Crafting a CURL PUT request\n"); + + curl_easy_setopt (hr->curl, + CURLOPT_UPLOAD, + 1L); + hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; + } + else if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_POST)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Crafting a CURL POST request\n"); + curl_easy_setopt (hr->curl, + CURLOPT_POST, + 1L); + curl_easy_setopt (hr->curl, + CURLOPT_VERBOSE, + 1L); + hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; + } + else if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_HEAD)) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + curl_easy_setopt (hr->curl, + CURLOPT_NOBODY, + 1L); + } + else if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_OPTIONS)) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + curl_easy_setopt (hr->curl, + CURLOPT_CUSTOMREQUEST, + "OPTIONS"); + } + else if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_GET)) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + curl_easy_setopt (hr->curl, + CURLOPT_HTTPGET, + 1L); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unsupported HTTP method `%s'\n", + meth); + curl_easy_cleanup (hr->curl); + hr->curl = NULL; + return MHD_NO; + } + + if (CURLM_OK != + curl_multi_add_handle (curl_multi, + hr->curl)) + { + GNUNET_break (0); + curl_easy_cleanup (hr->curl); + hr->curl = NULL; + return MHD_NO; + } + + MHD_get_connection_values (con, + MHD_HEADER_KIND, + &con_val_iter, + hr); + + curl_easy_setopt (hr->curl, + CURLOPT_HTTPHEADER, + hr->headers); + curl_download_prepare (); + + return MHD_YES; + } + + if (REQUEST_STATE_PROXY_DOWNLOAD_DONE != hr->state) + { + GNUNET_assert (GNUNET_NO == hr->suspended); + MHD_suspend_connection (con); + hr->suspended = GNUNET_YES; + return MHD_YES; /* wait for curl */ + } + + GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_DONE == hr->state); + + hr->response + = MHD_create_response_from_buffer_copy (hr->io_len, + hr->io_buf); + for (struct HttpResponseHeader *header = hr->header_head; + NULL != header; + header = header->next) + { + const char *value = header->value; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding MHD response header %s->%s\n", + header->type, + value); + GNUNET_break (MHD_YES == + MHD_add_response_header (hr->response, + header->type, + value)); + } + TALER_MHD_daemon_trigger (); + + return MHD_queue_response (con, + hr->response_code, + hr->response); +} diff --git a/src/backend/paivana-httpd_reverse.h b/src/backend/paivana-httpd_reverse.h @@ -0,0 +1,81 @@ +/* + 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_reverse.h + * + * @brief Project data definition + */ +#ifndef PAIVANA_HTTPD_REVERSE_H +#define PAIVANA_HTTPD_REVERSE_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "paivana-httpd.h" + +bool +PAIVANA_HTTPD_reverse_init (void); + + +void +PAIVANA_HTTPD_reverse_shutdown (void); + + +struct HttpRequest * +PAIVANA_HTTPD_reverse_create (struct MHD_Connection *connection, + const char *url); + +/** + * Main MHD callback for reverse proxy. + * + * @param hr the HTTP request context + * @param con MHD connection handle + * @param url the url in the request + * @param meth the HTTP method used ("GET", "PUT", etc.) + * @param ver the HTTP version string (i.e. "HTTP/1.1") + * @param upload_data the data being uploaded (excluding HEADERS, + * for a POST that fits into memory and that is encoded + * with a supported encoding, the POST data will NOT be + * given in upload_data and is instead available as + * part of MHD_get_connection_values; very large POST + * data *will* be made available incrementally in + * upload_data) + * @param upload_data_size set initially to the size of the + * @a upload_data provided; the method must update this + * value to the number of bytes NOT processed; + * @return #MHD_YES if the connection was handled successfully, + * #MHD_NO if the socket must be closed due to a serious + * error while handling the request + */ +enum MHD_Result +PAIVANA_HTTPD_reverse (struct HttpRequest *hr, + struct MHD_Connection *con, + const char *url, + const char *meth, + const char *ver, + const char *upload_data, + size_t *upload_data_size); + + +void +PAIVANA_HTTPD_reverse_cleanup (struct HttpRequest *hr); + +#endif