paivana

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

commit 385df4ed691b0f877a681c290d02329e7306513b
parent 10949b07c2635129bf3517386878604c8ed84aed
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 26 Apr 2026 21:04:07 +0200

clean up reverse proxy state machine logic and error handling

Diffstat:
Msrc/backend/paivana-httpd_reverse.c | 1142+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
1 file changed, 683 insertions(+), 459 deletions(-)

diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c @@ -48,38 +48,72 @@ /** - * State machine for HTTP requests (per request). + * State machine for HTTP requests (per request). MHD invokes the + * access handler multiple times per request — we use this enum to + * know what each invocation is expected to do, and cascade between + * states in a single invocation when no new data from MHD is + * required. */ enum RequestState { /** - * We've started receiving upload data from MHD. - * Initial state. + * Initial state. MHD's first access-handler call (immediately + * after parsing the request headers, `upload_data_size == 0`) + * has not yet been observed. In this state we can still queue + * a final response before MHD auto-generates a 100 Continue and + * the client invariably starts with the upload (if any). + */ + REQUEST_STATE_HEADERS_PENDING, + + /** + * We have accepted the request and are receiving the + * body from the client. Each body chunk is buffered into + * `io_buf`; when MHD signals end-of-body (size == 0) we + * advance to `CLIENT_UPLOAD_DONE`. */ REQUEST_STATE_CLIENT_UPLOAD_STARTED, /** - * Wa have started uploading data to the proxied service. + * We have decided to reject the upload (Content-Length exceeded + * the buffer cap, or the actual body did) but MHD is still + * delivering body chunks and refuses to let us queue a response + * during `BODY_RECEIVING`. Silently drop the bytes here until + * MHD gets back to a state that allows `MHD_queue_response`. */ - REQUEST_STATE_PROXY_UPLOAD_STARTED, + REQUEST_STATE_REJECT_UPLOAD_DRAIN, + + /** + * Ready to queue the 413 Content Too Large response. + */ + REQUEST_STATE_REJECT_UPLOAD, /** - * We're done with the upload from MHD. + * Client body fully received; next step is to initialize the + * curl handle and start forwarding to the upstream. */ REQUEST_STATE_CLIENT_UPLOAD_DONE, /** + * We have started uploading data to the proxied service. + * MHD handling will be suspended. + */ + REQUEST_STATE_PROXY_UPLOAD_STARTED, + + /** * We're done uploading data to the proxied service. + * MHD handling should remain suspended. */ REQUEST_STATE_PROXY_UPLOAD_DONE, /** * We've finished uploading data via CURL and can now download. + * MHD handling should remain suspended. */ REQUEST_STATE_PROXY_DOWNLOAD_STARTED, /** - * We've finished receiving download data from cURL. + * We've finished receiving download data from cURL; ready to + * build and queue the final response to the client. */ REQUEST_STATE_PROXY_DOWNLOAD_DONE }; @@ -215,25 +249,6 @@ struct HttpRequest int curl_paused; /** - * Have we observed the initial `HEADERS_PROCESSED` (a.k.a. "first") - * access-handler call for this request yet? MHD invokes the - * handler once immediately after parsing the request headers with - * `upload_data_size == 0` (and no body data yet), then again later - * with body chunks (if any), and finally once more with - * `upload_data_size == 0` at `FULL_REQ_RECEIVED`. We must defer - * curl setup to the "final" call so that request bodies are - * available in `io_buf` by the time curl runs. - */ - bool accepted; - - /** - * Set when the request body exceeded `PH_request_buffer_max`. We - * must drain the rest of the upload silently and queue the 413 - * response on the "final" access-handler call. - */ - bool reject_upload; - - /** * 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 @@ -269,6 +284,21 @@ static struct HttpRequest *hr_tail; static struct MHD_Response *curl_failure_response; /** + * Response we return if the HTTP method is not allowed. + */ +static struct MHD_Response *method_failure_response; + +/** + * Response we return if the upload is too big. + */ +static struct MHD_Response *upload_failure_response; + +/** + * Response we return if we encountered an internal failure. + */ +static struct MHD_Response *internal_failure_response; + +/** * The cURL multi handle */ static CURLM *curl_multi; @@ -279,15 +309,59 @@ static CURLM *curl_multi; static struct GNUNET_SCHEDULER_Task *curl_download_task; +/** + * Create HTML response using @a body + * + * @param body UTF-8 encoded 0-terminated body to use + * @return NULL on error + */ +static struct MHD_Response * +make_html_response (const char *body) +{ + struct MHD_Response *ret; + + ret = MHD_create_response_from_buffer_static (strlen (body), + body); + if (NULL == ret) + { + GNUNET_break (0); + return NULL; + } + GNUNET_break (MHD_YES == + MHD_add_response_header (ret, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html; charset=utf-8")); + return ret; +} + + bool PAIVANA_HTTPD_reverse_init (void) { - static const char *failure_body = + static const char *curl_failure_body = "<!DOCTYPE html>\n" "<html><head><title>Bad Gateway</title></head>" "<body><h1>502 Bad Gateway</h1>" "<p>The upstream server could not be reached.</p>" "</body></html>\n"; + static const char *internal_failure_body = + "<!DOCTYPE html>\n" + "<html><head><title>Internal server failure</title></head>" + "<body><h1>500 Internal Server Failure</h1>" + "<p>The server experienced an internal failure.</p>" + "</body></html>\n"; + static const char *upload_failure_body = + "<!DOCTYPE html>\n" + "<html><head><title>Content too large</title></head>" + "<body><h1>413 Content too large</h1>" + "<p>The size of the body exceeds the limit.</p>" + "</body></html>\n"; + static const char *method_failure_body = + "<!DOCTYPE html>\n" + "<html><head><title>Method not allowed</title></head>" + "<body><h1>405 Method not allowed</h1>" + "<p>The HTTP method specified is not allowed.</p>" + "</body></html>\n"; if (0 != curl_global_init (CURL_GLOBAL_WIN32)) { @@ -302,18 +376,33 @@ PAIVANA_HTTPD_reverse_init (void) return false; } curl_failure_response - = MHD_create_response_from_buffer_static (strlen (failure_body), - failure_body); + = make_html_response (curl_failure_body); if (NULL == curl_failure_response) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to create cURL failure response!\n"); + GNUNET_break (0); + return false; + } + upload_failure_response + = make_html_response (upload_failure_body); + if (NULL == upload_failure_response) + { + GNUNET_break (0); + return false; + } + method_failure_response + = make_html_response (method_failure_body); + if (NULL == method_failure_response) + { + GNUNET_break (0); + return false; + } + internal_failure_response + = make_html_response (internal_failure_body); + if (NULL == internal_failure_response) + { + GNUNET_break (0); return false; } - GNUNET_break (MHD_YES == - MHD_add_response_header (curl_failure_response, - MHD_HTTP_HEADER_CONTENT_TYPE, - "text/html; charset=utf-8")); return true; } @@ -346,6 +435,21 @@ PAIVANA_HTTPD_reverse_shutdown (void) MHD_destroy_response (curl_failure_response); curl_failure_response = NULL; } + if (NULL != upload_failure_response) + { + MHD_destroy_response (upload_failure_response); + upload_failure_response = NULL; + } + if (NULL != method_failure_response) + { + MHD_destroy_response (method_failure_response); + method_failure_response = NULL; + } + if (NULL != internal_failure_response) + { + MHD_destroy_response (internal_failure_response); + internal_failure_response = NULL; + } } @@ -1082,6 +1186,7 @@ PAIVANA_HTTPD_reverse_create (struct MHD_Connection *connection, struct HttpRequest *hr; hr = GNUNET_new (struct HttpRequest); + hr->state = REQUEST_STATE_HEADERS_PENDING; hr->con = connection; hr->url = GNUNET_strdup (url); GNUNET_CONTAINER_DLL_insert (hr_head, @@ -1143,474 +1248,472 @@ PAIVANA_HTTPD_reverse_cleanup (struct HttpRequest *hr) /** - * Main MHD callback for reverse proxy. + * Parse the client's Content-Length header (if any) and decide + * whether the declared body size fits within our per-request + * buffer cap. Returns false if the header is present, well-formed, + * and exceeds `PH_request_buffer_max`; the caller must then + * transition to the reject path to suppress the implicit 100 + * Continue and avoid buffering a body we would only throw away. * - * @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 + * @param con MHD connection to look up the header on + * @return true if OK to continue accepting the body */ -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) +static bool +content_length_ok (struct MHD_Connection *con) { - /* MHD's "first" call (immediately after headers) arrives with - `upload_data_size == 0` and no body. Just acknowledge it so - MHD will proceed to deliver the request body (if any) and then - make the "final" call. Setting up curl here would start the - upstream request with an empty io_buf and leave body chunks - nowhere to land. */ - if (! hr->accepted) + const char *cl_str; + char *endptr; + unsigned long long cl; + + cl_str = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if (NULL == cl_str) + return true; + errno = 0; + cl = strtoull (cl_str, + &endptr, + 10); + if ( (0 != errno) || + ('\0' != *endptr) ) + return true; /* unparseable — defer judgment to the drain path */ + if (cl <= PH_request_buffer_max) + return true; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Rejecting upload: Content-Length %llu exceeds %llu byte limit\n", + cl, + PH_request_buffer_max); + return false; +} + + +/** + * Append an upload chunk from the client into `hr->io_buf`, growing + * the buffer as needed. Returns false when the chunk would push + * the total past `PH_request_buffer_max`; the caller must then + * transition to the drain path (MHD disallows queuing a response + * while `BODY_RECEIVING`, so we silently discard the rest and + * queue the 413 once MHD calls us back at `FULL_REQ_RECEIVED`). + * + * @param[in,out] hr request we are handling + * @param upload_data_size number of bytes in @a upload_data + * @param upload_data data being uploaded + * @return true on success, false if the upload is too big + */ +static bool +buffer_upload_chunk (struct HttpRequest *hr, + size_t upload_data_size, + const char upload_data[static upload_data_size]) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Processing %u bytes UPLOAD\n", + (unsigned int) upload_data_size); + if (hr->io_len + upload_data_size > PH_request_buffer_max) { - /* If the client declared a Content-Length we already know is too - large, reject now: MHD is in HEADERS_PROCESSED so we can queue - a response, and rejecting here suppresses the implicit 100 - Continue (RFC 7231 §5.1.1) for clients that asked for one and - avoids buffering the body just to throw it away. Chunked - uploads (no Content-Length) and clients that ignore 100 - Continue and stream the body anyway still hit the - drain-then-reject path further down. */ - const char *cl_str - = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_CONTENT_LENGTH); - hr->accepted = true; - if (NULL != cl_str) - { - char *endptr; - unsigned long long cl; - - errno = 0; - cl = strtoull (cl_str, - &endptr, - 10); - if ( (0 == errno) && - ('\0' == *endptr) && - (cl > PH_request_buffer_max) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Rejecting upload: Content-Length %llu exceeds %llu byte limit\n", - cl, - PH_request_buffer_max); - hr->reject_upload = true; - return MHD_queue_response (con, - MHD_HTTP_CONTENT_TOO_LARGE, - curl_failure_response); - } - } - return MHD_YES; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Upload exceeds %llu byte limit, rejecting\n", + PH_request_buffer_max); + return false; } - /* On the "final" access-handler call after we drained an - over-sized upload, queue the deferred 413 response now that - MHD is back in a state where that is allowed. */ - if (hr->reject_upload && 0 == *upload_data_size) + if (hr->io_size - hr->io_len < upload_data_size) { - return MHD_queue_response (con, - MHD_HTTP_CONTENT_TOO_LARGE, - curl_failure_response); + GNUNET_assert (hr->io_size * 2 + 1024 > hr->io_size); + 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)); } - /* 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); - - /* Reject uploads that would exceed our buffering cap. The - entire request body is currently buffered before forwarding, - so this also bounds memory usage per request. - MHD does not allow queuing a response while BODY_RECEIVING - (MHD_queue_response returns MHD_NO outside of - HEADERS_PROCESSED / FULL_REQ_RECEIVED), so we mark the - request as over-limit, silently drain the remaining body, - and queue the 413 on the "final" access-handler call. */ - if (hr->reject_upload || - hr->io_len + *upload_data_size > PH_request_buffer_max) - { - if (! hr->reject_upload) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Upload exceeds %llu byte limit, rejecting\n", - PH_request_buffer_max); - hr->reject_upload = true; - } - *upload_data_size = 0; - return MHD_YES; - } - - /* 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); + GNUNET_memcpy (&hr->io_buf[hr->io_len], + upload_data, + upload_data_size); + hr->io_len += upload_data_size; + return true; +} - hr->io_len += *upload_data_size; - *upload_data_size = 0; +/** + * Choose the curl options for the HTTP method we're proxying and + * set the next proxy state accordingly. Queues an error response + * and returns the corresponding MHD_Result for unsupported methods; + * otherwise returns #MHD_YES and leaves @a hr->state advanced to + * either #PROXY_UPLOAD_STARTED (method has a body to forward) or + * #PROXY_DOWNLOAD_STARTED (bodyless request). + * + * On error, the "curl" handle is set to NULL (!). + * + * @param[in,out] hr the request + * @param con client connection handle from MHD + * @param meth HTTP method specified by the client + * @return MHD status to return + */ +static enum MHD_Result +configure_curl_method (struct HttpRequest *hr, + struct MHD_Connection *con, + const char *meth) +{ + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_GET)) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + curl_easy_setopt (hr->curl, + CURLOPT_HTTPGET, + 1L); return MHD_YES; } - - /* Upload (*from the client*) finished or just a without-body - * request. */ - if (REQUEST_STATE_CLIENT_UPLOAD_STARTED == hr->state) + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_POST)) { - hr->state = REQUEST_STATE_CLIENT_UPLOAD_DONE; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Finished processing UPLOAD\n"); + "Crafting a CURL POST request\n"); + curl_easy_setopt (hr->curl, + CURLOPT_POST, + 1L); + hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; + return MHD_YES; } - - /* generate curl request to the proxied service. */ - if (NULL == hr->curl) + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_HEAD)) { - 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); + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; curl_easy_setopt (hr->curl, - CURLOPT_NOSIGNAL, + CURLOPT_NOBODY, 1L); + return MHD_YES; + } + 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_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); + CURLOPT_UPLOAD, + 1L); + hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; + return MHD_YES; + } + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_DELETE)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Crafting a CURL DELETE request\n"); curl_easy_setopt (hr->curl, - CURLOPT_WRITEDATA, - hr); - { - char *curlurl; - char *host_hdr; - - GNUNET_asprintf (&curlurl, - "%s%s", - PH_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 (PH_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); - } - - 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); - hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; - } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_PATCH)) + CURLOPT_CUSTOMREQUEST, + "DELETE"); + if (0 != hr->io_len) { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Crafting a CURL PATCH request\n"); - /* CURLOPT_POST=1 turns on body upload via the read callback; - CURLOPT_CUSTOMREQUEST then overrides the verb on the wire. */ + /* DELETE with a request body is unusual but legal. */ curl_easy_setopt (hr->curl, CURLOPT_POST, 1L); - curl_easy_setopt (hr->curl, - CURLOPT_CUSTOMREQUEST, - "PATCH"); hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; } - else if (0 == strcasecmp (meth, - MHD_HTTP_METHOD_DELETE)) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Crafting a CURL DELETE request\n"); - curl_easy_setopt (hr->curl, - CURLOPT_CUSTOMREQUEST, - "DELETE"); - if (0 != hr->io_len) - { - /* DELETE with a request body is unusual but legal. */ - curl_easy_setopt (hr->curl, - CURLOPT_POST, - 1L); - hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; - } - else - { - hr->state = REQUEST_STATE_PROXY_DOWNLOAD_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 { - /* TRACE leaks headers back to the client; CONNECT is for - TLS tunnelling and doesn't fit the reverse-proxy model. - Reject anything else with a proper 405. */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Unsupported HTTP method `%s'\n", - meth); - curl_easy_cleanup (hr->curl); - hr->curl = NULL; - return MHD_queue_response (con, - MHD_HTTP_METHOD_NOT_ALLOWED, - curl_failure_response); + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; } + return MHD_YES; + } + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_PATCH)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Crafting a CURL PATCH request\n"); + /* CURLOPT_POST=1 turns on body upload via the read callback; + CURLOPT_CUSTOMREQUEST then overrides the verb on the wire. */ + curl_easy_setopt (hr->curl, + CURLOPT_POST, + 1L); + curl_easy_setopt (hr->curl, + CURLOPT_CUSTOMREQUEST, + "PATCH"); + hr->state = REQUEST_STATE_PROXY_UPLOAD_STARTED; + return MHD_YES; + } + if (0 == strcasecmp (meth, + MHD_HTTP_METHOD_OPTIONS)) + { + hr->state = REQUEST_STATE_PROXY_DOWNLOAD_STARTED; + curl_easy_setopt (hr->curl, + CURLOPT_CUSTOMREQUEST, + "OPTIONS"); + return MHD_YES; + } + /* TRACE leaks headers back to the client; CONNECT is for TLS + tunnelling and doesn't fit the reverse-proxy model. Reject + anything else with a proper 405. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unsupported HTTP method `%s'\n", + meth); + curl_easy_cleanup (hr->curl); + hr->curl = NULL; + return MHD_queue_response (con, + MHD_HTTP_METHOD_NOT_ALLOWED, + method_failure_response); +} - 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); - } - /* 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, - MHD_HEADER_KIND, - &collect_proxy_state, - hr); - MHD_get_connection_values (con, - MHD_HEADER_KIND, - &con_val_iter, - hr); +/** + * Attach the reverse-proxy forwarding headers (X-Forwarded-For / + * -Proto / -Host and Via) to `hr->headers`. Our X-Forwarded-* + * replace any client-supplied values (filtered in `con_val_iter`); + * Via is appended to whatever chain the client already carried, + * per RFC 9110 §7.6.3. + * + * @param[in,out] hr the request + * @param con MHD connection we are processing + * @param ver HTTP version of the client, as given by MHD + */ +static void +append_forwarded_headers (struct HttpRequest *hr, + struct MHD_Connection *con, + const char *ver) +{ + const union MHD_ConnectionInfo *ci; + char *hdr; + const char *proto; + const char *fhost; + const char *via_ver = "1.1"; + + ci = MHD_get_connection_info (con, + MHD_CONNECTION_INFO_CLIENT_ADDRESS); + if ( (NULL != ci) && + (NULL != ci->client_addr) ) + { + char ipbuf[INET6_ADDRSTRLEN]; + const char *ip = NULL; - /* Add standard reverse-proxy forwarding headers. X-Forwarded-* - and Forwarded are replaced with our view of the connection - (filtered out in con_val_iter); Via is *appended to* the - client's existing chain per RFC 9110 §7.6.3. */ + switch (ci->client_addr->sa_family) + { + case AF_INET: + ip = inet_ntop ( + AF_INET, + &((const struct sockaddr_in *) ci->client_addr)->sin_addr, + ipbuf, sizeof (ipbuf)); + break; + case AF_INET6: + ip = inet_ntop ( + AF_INET6, + &((const struct sockaddr_in6 *) ci->client_addr)->sin6_addr, + ipbuf, sizeof (ipbuf)); + break; + default: + break; + } + if (NULL != ip) { - const union MHD_ConnectionInfo *ci; - char *hdr; - const char *proto; - const char *fhost; - - ci = MHD_get_connection_info (con, - MHD_CONNECTION_INFO_CLIENT_ADDRESS); - if ( (NULL != ci) && (NULL != ci->client_addr) ) - { - char ipbuf[INET6_ADDRSTRLEN]; - const char *ip = NULL; - - switch (ci->client_addr->sa_family) - { - case AF_INET: - ip = inet_ntop ( - AF_INET, - &((const struct sockaddr_in *) ci->client_addr)->sin_addr, - ipbuf, sizeof (ipbuf)); - break; - case AF_INET6: - ip = inet_ntop ( - AF_INET6, - &((const struct sockaddr_in6 *) ci->client_addr)->sin6_addr, - ipbuf, sizeof (ipbuf)); - break; - default: - break; - } - if (NULL != ip) - { - GNUNET_asprintf (&hdr, - "X-Forwarded-For: %s", - ip); - hr->headers = curl_slist_append (hr->headers, - hdr); - GNUNET_free (hdr); - } - } - proto = (GNUNET_YES == TALER_mhd_is_https (con)) - ? "https" : "http"; GNUNET_asprintf (&hdr, - "X-Forwarded-Proto: %s", - proto); - hr->headers = curl_slist_append (hr->headers, - hdr); - GNUNET_free (hdr); - fhost = MHD_lookup_connection_value (con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_HOST); - if (NULL != fhost) - { - GNUNET_asprintf (&hdr, - "X-Forwarded-Host: %s", - fhost); - hr->headers = curl_slist_append (hr->headers, - hdr); - GNUNET_free (hdr); - } - /* Via: pseudonym + protocol-version (RFC 9110 §7.6.3); - MHD hands us e.g. "HTTP/1.1" but Via wants just "1.1". - If the client already carried a Via chain, prepend it so - the upstream sees the full trace of proxies. */ - { - const char *via_ver = "1.1"; - - if ( (NULL != ver) && - (0 == strncasecmp (ver, "HTTP/", 5)) ) - via_ver = ver + 5; - if (NULL != hr->client_via) - GNUNET_asprintf (&hdr, - "%s: %s, %s paivana", - MHD_HTTP_HEADER_VIA, - hr->client_via, - via_ver); - else - GNUNET_asprintf (&hdr, - "%s: %s paivana", - MHD_HTTP_HEADER_VIA, - via_ver); - } + "X-Forwarded-For: %s", + ip); hr->headers = curl_slist_append (hr->headers, hdr); GNUNET_free (hdr); } + } + proto = (GNUNET_YES == TALER_mhd_is_https (con)) + ? "https" : "http"; + GNUNET_asprintf (&hdr, + "X-Forwarded-Proto: %s", + proto); + hr->headers = curl_slist_append (hr->headers, + hdr); + GNUNET_free (hdr); + fhost = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_HOST); + if (NULL != fhost) + { + GNUNET_asprintf (&hdr, + "X-Forwarded-Host: %s", + fhost); + hr->headers = curl_slist_append (hr->headers, + hdr); + GNUNET_free (hdr); + } + /* MHD hands us e.g. "HTTP/1.1" but Via wants just "1.1". */ + if ( (NULL != ver) && + (0 == strncasecmp (ver, + "HTTP/", + strlen ("HTTP/"))) ) + via_ver = ver + 5; + if (NULL != hr->client_via) + GNUNET_asprintf (&hdr, + "%s: %s, %s paivana", + MHD_HTTP_HEADER_VIA, + hr->client_via, + via_ver); + else + GNUNET_asprintf (&hdr, + "%s: %s paivana", + MHD_HTTP_HEADER_VIA, + via_ver); + hr->headers = curl_slist_append (hr->headers, + hdr); + GNUNET_free (hdr); +} + +/** + * Initialize the curl handle, attach the forwarding headers, and + * hand the request off to the curl multi loop. On success + * advances @a hr->state to one of the PROXY_*_STARTED states and + * returns #MHD_YES. On any failure queues an appropriate error + * response and returns its MHD_Result. + * + * On error, the "curl" handle is set to NULL (!). + * + * @param[in,out] hr request we are handling + * @param con MHD connection handle + * @param meth HTTP method of the request + * @param ver HTTP version to use + * @return MHD status code to return + */ +static enum MHD_Result +start_curl_request (struct HttpRequest *hr, + struct MHD_Connection *con, + const char *meth, + const char *ver) +{ + enum MHD_Result r; + + 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, + internal_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; + + GNUNET_asprintf (&curlurl, + "%s%s", + PH_target_server_base_url, + hr->url); curl_easy_setopt (hr->curl, - CURLOPT_HTTPHEADER, - hr->headers); - curl_download_prepare (); + CURLOPT_URL, + curlurl); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Forwarding request to: %s\n", + curlurl); + GNUNET_free (curlurl); + } - return MHD_YES; + { + char *host_hdr; + + 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, + host_hdr); + GNUNET_free (host_hdr); } - if (REQUEST_STATE_PROXY_DOWNLOAD_DONE != hr->state) + r = configure_curl_method (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_assert (GNUNET_NO == hr->suspended); - MHD_suspend_connection (con); - hr->suspended = GNUNET_YES; - return MHD_YES; /* wait for curl */ + GNUNET_break (0); + curl_easy_cleanup (hr->curl); + hr->curl = NULL; + // FIXME: body is not perfect here... + return MHD_queue_response (con, + MHD_HTTP_BAD_GATEWAY, + curl_failure_response); } - GNUNET_assert (REQUEST_STATE_PROXY_DOWNLOAD_DONE == hr->state); + /* 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, + MHD_HEADER_KIND, + &collect_proxy_state, + hr); + MHD_get_connection_values (con, + MHD_HEADER_KIND, + &con_val_iter, + hr); + append_forwarded_headers (hr, + con, + ver); + + curl_easy_setopt (hr->curl, + CURLOPT_HTTPHEADER, + hr->headers); + curl_download_prepare (); + return MHD_YES; +} + - /* Response may already be set to curl_failure_response by the - curl task on upstream failure; in that case, don't build a +/** + * Build the final MHD response from the accumulated upstream body + * (or the pre-built failure page, on curl error) and queue it. + * + * @param[in,out] request handle + * @param con MHD client connection to send response on + */ +static enum MHD_Result +finalize_response (struct HttpRequest *hr, + struct MHD_Connection *con) +{ + /* `hr->response` may already be set to `curl_failure_response` by + the curl task on upstream failure; in that case, don't build a buffer response and don't attach per-request headers to the shared failure response. */ if (NULL == hr->response) @@ -1622,7 +1725,7 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr, { GNUNET_break (0); hr->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; - hr->response = curl_failure_response; + hr->response = internal_failure_response; } else { @@ -1642,8 +1745,129 @@ PAIVANA_HTTPD_reverse (struct HttpRequest *hr, } } TALER_MHD_daemon_trigger (); - return MHD_queue_response (con, hr->response_code, hr->response); } + + +/** + * Main MHD callback for reverse proxy. + * + * Pure state-machine dispatch: each invocation picks up @a hr->state, + * runs the transitions it can without new input from MHD, and + * either returns to wait for more MHD data / curl progress, or + * cascades through the `while` loop to the next applicable state. + * + * @param hr the HTTP request context + * @param con MHD connection handle + * @param url the url in the request (unused; kept for ABI symmetry + * with the MHD access handler signature) + * @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) + * @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) url; + + while (true) + { + switch (hr->state) + { + case REQUEST_STATE_HEADERS_PENDING: + /* MHD's HEADERS_PROCESSED callback: we can still queue a + response here before the client body is consumed, so this + is our only chance to short-circuit an oversized upload + based on Content-Length and suppress the implicit 100 + Continue (RFC 7231 §5.1.1). */ + if (! content_length_ok (con)) + { + hr->state = REQUEST_STATE_REJECT_UPLOAD; + continue; + } + hr->state = REQUEST_STATE_CLIENT_UPLOAD_STARTED; + return MHD_YES; + + case REQUEST_STATE_CLIENT_UPLOAD_STARTED: + if (0 == *upload_data_size) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Finished processing UPLOAD\n"); + hr->state = REQUEST_STATE_CLIENT_UPLOAD_DONE; + continue; + } + if (buffer_upload_chunk (hr, + *upload_data_size, + upload_data)) + { + *upload_data_size = 0; + return MHD_YES; + } + hr->state = REQUEST_STATE_REJECT_UPLOAD_DRAIN; + continue; + + case REQUEST_STATE_REJECT_UPLOAD_DRAIN: + /* MHD disallows queuing a response while BODY_RECEIVING, so + silently discard the remaining body bytes. Once MHD calls + us with `upload_data_size == 0` (FULL_REQ_RECEIVED) we can + transition to REJECT_UPLOAD and queue the deferred 413. */ + if (0 != *upload_data_size) + { + *upload_data_size = 0; + return MHD_YES; + } + hr->state = REQUEST_STATE_REJECT_UPLOAD; + continue; + + case REQUEST_STATE_REJECT_UPLOAD: + return MHD_queue_response (con, + MHD_HTTP_CONTENT_TOO_LARGE, + upload_failure_response); + + case REQUEST_STATE_CLIENT_UPLOAD_DONE: + /* 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. */ + 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, + how did we get here? */ + GNUNET_break (0); + GNUNET_assert (GNUNET_NO == hr->suspended); + MHD_suspend_connection (con); + hr->suspended = GNUNET_YES; + return MHD_YES; + + case REQUEST_STATE_PROXY_DOWNLOAD_DONE: + return finalize_response (hr, + con); + } + GNUNET_assert (0); /* unreachable */ + } +}