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:
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 */
+ }
+}