paivana

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

commit 9702a31471076bb244d8d5cf448053de7c23cdf2
parent 63c5ea79ca40beb4d6418c45dc78927897648ac4
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon, 27 Apr 2026 00:12:04 +0200

remove -F option, also use PH_ctx instead of another curl multi loop for the reverse proxy curl interaction

Diffstat:
Msrc/backend/paivana-httpd.c | 8--------
Msrc/backend/paivana-httpd.h | 9---------
Msrc/backend/paivana-httpd_reverse.c | 485++++++++++++++-----------------------------------------------------------------
3 files changed, 87 insertions(+), 415 deletions(-)

diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c @@ -52,8 +52,6 @@ struct GNUNET_CURL_Context *PH_ctx; int PH_no_check; -int PH_force_complete_upload; - int PH_respect_forwarded_headers; unsigned long long PH_request_buffer_max = 1024 * 1024; @@ -314,12 +312,6 @@ main (int argc, "trust X-Forwarded-For for the client address (only safe behind a trusted reverse proxy)"), &PH_respect_forwarded_headers), GNUNET_GETOPT_option_flag ( - 'F', - "force-complete-upload", - gettext_noop ( - "force Paivana to finish the upload even if the server has already started to respond"), - &PH_force_complete_upload), - GNUNET_GETOPT_option_flag ( 'n', "no-payment", gettext_noop ( diff --git a/src/backend/paivana-httpd.h b/src/backend/paivana-httpd.h @@ -65,15 +65,6 @@ extern struct GNUNET_CURL_Context *PH_ctx; extern int PH_no_check; /** - * Force Paivana to complete an upload even if the - * server already started sending a response. This MAY - * fix some data integrity issues, but also could be - * unsupported by some servers and be wasteful if the - * server has already rejected the upload. - */ -extern int PH_force_complete_upload; - -/** * If set, derive the client address from the leftmost entry of the * "X-Forwarded-For" request header (falling back to the socket * address only when the header is absent). Only enable this when diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c @@ -29,6 +29,7 @@ #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_reverse.h" @@ -115,7 +116,13 @@ enum RequestState * We've finished receiving download data from cURL; ready to * build and queue the final response to the client. */ - REQUEST_STATE_PROXY_DOWNLOAD_DONE + REQUEST_STATE_PROXY_DOWNLOAD_DONE, + + /** + * We've failed to download data from cURL; ready to + * build and queue the final response to the client. + */ + REQUEST_STATE_PROXY_DOWNLOAD_FAILED }; @@ -198,6 +205,11 @@ struct HttpRequest CURL *curl; /** + * Job handle for the CURL request in our event loop. + */ + struct GNUNET_CURL_Job *job; + + /** * HTTP request headers for the curl request. */ struct curl_slist *headers; @@ -244,11 +256,6 @@ struct HttpRequest enum GNUNET_GenericReturnValue suspended; /** - * Did we pause CURL processing? - */ - bool curl_paused; - - /** * Concatenated value of the client's Via header(s), if any. Per * RFC 9110 §7.6.3 a proxy must *append* its own entry to this * list, not replace it; we capture the inbound value here before @@ -298,16 +305,6 @@ static struct MHD_Response *upload_failure_response; */ static struct MHD_Response *internal_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; - /** * Create HTML response using @a body @@ -363,18 +360,6 @@ PAIVANA_HTTPD_reverse_init (void) "<p>The HTTP method specified is not allowed.</p>" "</body></html>\n"; - 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; - } curl_failure_response = make_html_response (curl_failure_body); if (NULL == curl_failure_response) @@ -420,16 +405,6 @@ PAIVANA_HTTPD_reverse_shutdown (void) MHD_resume_connection (hr->con); } } - if (NULL != curl_download_task) - { - GNUNET_SCHEDULER_cancel (curl_download_task); - curl_download_task = NULL; - } - if (NULL != curl_multi) - { - curl_multi_cleanup (curl_multi); - curl_multi = NULL; - } if (NULL != curl_failure_response) { MHD_destroy_response (curl_failure_response); @@ -681,117 +656,66 @@ curl_check_hdr (void *buffer, /** - * 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. + * Handle response payload data from cURL. * - * @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. + * @param cls our `struct HttpRequest *` + * @param response_code HTTP status returned by the server + * @param body response body + * @param body_size number of blocks of data */ -static enum GNUNET_GenericReturnValue -create_mhd_response_from_hr (struct HttpRequest *hr) +static void +curl_download_cb (void *cls, + long response_code, + const void *body, + size_t body_size) { - long resp_code; + struct HttpRequest *hr = cls; - if (NULL != hr->response) + hr->job = NULL; + if (0 == response_code) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Response already set!\n"); - return GNUNET_SYSERR; + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_FAILED; } - 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) + else { - if (0 == hr->io_len) + hr->response = MHD_create_response_from_buffer_copy (body_size, + body); + hr->response_code = response_code; + for (struct HttpResponseHeader *header = hr->header_head; + NULL != header; + header = header->next) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding MHD response header %s->%s\n", + header->type, + header->value); + GNUNET_break (MHD_YES == + MHD_add_response_header (hr->response, + header->type, + header->value)); + } + if ( (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == hr->state) || + ( (REQUEST_STATE_PROXY_UPLOAD_STARTED == hr->state) && + (0 == hr->io_len) ) ) { /* Either the request body was empty (CURLOPT_POSTFIELDSIZE = 0, so libcurl never called our read callback) or the upload already drained but we have not yet entered upload_cb to flip the state — either way, the upload is complete and we can move straight on to consuming the response. */ - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; - } - else - { - /* HTTP/1.1 lets the upstream begin responding before we have - finished sending the request body. Pause the download - callback (libcurl will buffer the response bytes for us) - and let curl_upload_cb finish draining io_buf; it will - resume us via curl_easy_pause() once io_len hits zero. */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Pausing download cb, %u upload bytes still to send\n", - (unsigned int) hr->io_len); - hr->curl_paused = true; - return CURL_WRITEFUNC_PAUSE; + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; } } - GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_STARTED == - hr->state); - if (hr->io_size - hr->io_len < total) + if (GNUNET_YES == hr->suspended) { - 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)); + hr->suspended = GNUNET_NO; + MHD_resume_connection (hr->con); + TALER_MHD_daemon_trigger (); } - 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. * @@ -853,26 +777,11 @@ curl_upload_cb (void *buf, &hr->io_buf[to_copy], hr->io_len - to_copy); hr->io_len -= to_copy; - if ( (hr->curl_paused) && - (! PH_force_complete_upload) ) - { - hr->io_len = 0; - } if (0 == hr->io_len) { hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Completed CURL UPLOAD\n"); - if (hr->curl_paused) - { - /* curl_download_cb pauses itself when an early upstream - response arrives mid-upload; resume it now that the - request body has been fully delivered. */ - hr->curl_paused = false; - curl_easy_pause (hr->curl, - CURLPAUSE_CONT); - curl_download_prepare (); - } } return to_copy; } @@ -914,93 +823,6 @@ build_host_header (const char *url) /** - * 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. * @@ -1081,113 +903,6 @@ con_val_iter (void *cls, } -/** - * 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 (0 == hr->response_code) - 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)); - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_DONE; - /* Surface upstream failures as 502 Bad Gateway. Drop - any partial body we might have accumulated and let - MHD return our pre-built failure page instead of - half a truncated upstream response. */ - hr->response_code = MHD_HTTP_BAD_GATEWAY; - hr->io_len = 0; - hr->response = curl_failure_response; - if (GNUNET_YES == hr->suspended) - { - MHD_resume_connection (hr->con); - hr->suspended = GNUNET_NO; - } - break; - } /* end switch (msg->data.result) */ - 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; - } /* end switch msg->msg */ - } /* end while (curl_multi_info_read()) */ - } 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) @@ -1214,12 +929,15 @@ PAIVANA_HTTPD_reverse_cleanup (struct HttpRequest *hr) { 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->job) + { + GNUNET_CURL_job_cancel (hr->job); + hr->job = NULL; + } if (NULL != hr->headers) { curl_slist_free_all (hr->headers); @@ -1592,6 +1310,8 @@ start_curl_request (struct HttpRequest *hr, if (NULL == hr->curl) { PAIVANA_LOG_ERROR ("Could not init the curl handle\n"); + hr->suspended = GNUNET_NO; + MHD_resume_connection (hr->con); return MHD_queue_response (con, MHD_HTTP_INTERNAL_SERVER_ERROR, internal_failure_response); @@ -1636,16 +1356,6 @@ start_curl_request (struct HttpRequest *hr, 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); - if (PH_force_complete_upload) - curl_easy_setopt (hr->curl, - CURLOPT_KEEP_SENDING_ON_ERROR, - 1); { char *curlurl; @@ -1677,20 +1387,11 @@ start_curl_request (struct HttpRequest *hr, con, meth); if (NULL == hr->curl) - return r; /* unsupported method: response already queued */ - - if (CURLM_OK != - curl_multi_add_handle (curl_multi, - hr->curl)) { - GNUNET_break (0); - curl_easy_cleanup (hr->curl); - hr->curl = NULL; - return MHD_queue_response (con, - MHD_HTTP_BAD_GATEWAY, - curl_failure_response); + hr->suspended = GNUNET_NO; + MHD_resume_connection (hr->con); + return r; /* unsupported method: response already queued */ } - /* First pass: collect Via / Connection so `con_val_iter` can honor them (append to Via; drop headers named by Connection). */ MHD_get_connection_values (con, @@ -1704,11 +1405,21 @@ start_curl_request (struct HttpRequest *hr, append_forwarded_headers (hr, con, ver); - - curl_easy_setopt (hr->curl, - CURLOPT_HTTPHEADER, - hr->headers); - curl_download_prepare (); + hr->job = GNUNET_CURL_job_add_raw (PH_ctx, + hr->curl, + hr->headers, + &curl_download_cb, + hr); + hr->curl = NULL; + if (NULL == hr->job) + { + GNUNET_break (0); + hr->suspended = GNUNET_NO; + MHD_resume_connection (hr->con); + return MHD_queue_response (con, + MHD_HTTP_BAD_GATEWAY, + curl_failure_response); + } return MHD_YES; } @@ -1730,33 +1441,10 @@ finalize_response (struct HttpRequest *hr, shared failure response. */ if (NULL == hr->response) { - hr->response - = MHD_create_response_from_buffer_static (hr->io_len, - hr->io_buf); - if (NULL == hr->response) - { - GNUNET_break (0); - hr->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - hr->response = internal_failure_response; - } - else - { - for (struct HttpResponseHeader *header = hr->header_head; - NULL != header; - header = header->next) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Adding MHD response header %s->%s\n", - header->type, - header->value); - GNUNET_break (MHD_YES == - MHD_add_response_header (hr->response, - header->type, - header->value)); - } - } + GNUNET_break (0); + hr->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + hr->response = internal_failure_response; } - TALER_MHD_daemon_trigger (); return MHD_queue_response (con, hr->response_code, hr->response); @@ -1854,18 +1542,15 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr, /* start_curl_request advances state to PROXY_UPLOAD_STARTED or PROXY_DOWNLOAD_STARTED on success and returns MHD_YES; on error it queues a response and returns its MHD_Result. */ + GNUNET_assert (GNUNET_NO == hr->suspended); + MHD_suspend_connection (con); + hr->suspended = GNUNET_YES; return start_curl_request (hr, con, meth, ver); case REQUEST_STATE_PROXY_UPLOAD_STARTED: - /* curl is still working; suspend the connection until the - curl task resumes us with state == PROXY_DOWNLOAD_DONE. */ - GNUNET_assert (GNUNET_NO == hr->suspended); - MHD_suspend_connection (con); - hr->suspended = GNUNET_YES; - return MHD_YES; case REQUEST_STATE_PROXY_UPLOAD_DONE: case REQUEST_STATE_PROXY_DOWNLOAD_STARTED: /* we should not have been resumed in this state, @@ -1879,6 +1564,10 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr, case REQUEST_STATE_PROXY_DOWNLOAD_DONE: return finalize_response (hr, con); + case REQUEST_STATE_PROXY_DOWNLOAD_FAILED: + return MHD_queue_response (con, + MHD_HTTP_BAD_GATEWAY, + curl_failure_response); } GNUNET_assert (0); /* unreachable */ }