commit 379078c615854f8850a069dd7fee78827e376e12
parent 5dfdaf05ea095c7d26e1a48e76bb2a04d2c62191
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 4 Aug 2026 16:53:37 +0200
make URL parsing more robust, using libcurl parser
Diffstat:
5 files changed, 90 insertions(+), 23 deletions(-)
diff --git a/README b/README
@@ -46,7 +46,7 @@ Dependencies
- GNUnet (libgnunetutil, libgnunetcurl)
- libmicrohttpd
-- libcurl >= 7.34.0
+- libcurl >= 7.62.0
- libjansson
- libgcrypt >= 1.6.1
- zlib
diff --git a/meson.build b/meson.build
@@ -110,13 +110,15 @@ if not get_option('only-doc')
gnunetjson_dep = cc.find_library('gnunetjson', required: true)
endif
- curl_dep = dependency('libcurl', version: '>=7.34.0', required: false)
+ # 7.62.0 is where the curl_url_*() parser appeared; we use it to
+ # derive the upstream Host header.
+ curl_dep = dependency('libcurl', version: '>=7.62.0', required: false)
if not curl_dep.found()
curl_dep = cc.find_library('curl', required: true)
curl_version_check = '''#include <curl/curl.h>
int main(int argc, char **argv) {
- #if LIBCURL_VERSION_NUM < 0x073400
- #error "cURL version >= 7.34.0 required"
+ #if LIBCURL_VERSION_NUM < 0x073e00
+ #error "cURL version >= 7.62.0 required"
#endif
return 0;
}
@@ -126,7 +128,7 @@ if not get_option('only-doc')
name: 'cURL version check',
dependencies: curl_dep,
)
- error('cURL version >=7.34.0 required')
+ error('cURL version >=7.62.0 required')
endif
endif
diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c
@@ -863,36 +863,69 @@ curl_upload_cb (void *buf,
/* ************** helper functions ************* */
/**
- * Extract the hostname from a complete URL.
+ * Build the `Host:` header line to send upstream, naming the
+ * authority of @a url.
+ *
+ * The value is the host and — only if @a url states one explicitly —
+ * the port. Everything else in the authority must be left out: RFC
+ * 9110 §7.2 forbids userinfo in `Host:`, so a base URL of
+ * "http://user:pw@origin/" still has to yield "Host: origin". An
+ * IPv6 literal conversely has to *keep* its brackets (RFC 3986
+ * §3.2.2), as the colons would otherwise be read as a port
+ * delimiter. Both are why the authority is taken from libcurl's URL
+ * parser instead of being cut out of the string by hand.
*
* @param url full fledged URL
- * @return pointer to the 0-terminated hostname, to be freed
- * by the caller.
+ * @return the header line, or NULL if @a url has no usable
+ * authority; to be freed by the caller
*/
static char *
build_host_header (const char *url)
{
- #define MARKER "://"
-
+ CURLU *h;
+ char *host;
+ char *port = NULL;
char *header;
- char *end;
- char *hostname;
- char *dup = GNUNET_strdup (url);
- hostname = strstr (dup,
- MARKER);
- if (NULL == hostname)
+ h = curl_url ();
+ if (NULL == h)
{
- GNUNET_free (dup);
+ GNUNET_break (0);
+ return NULL;
+ }
+ if ( (CURLUE_OK !=
+ curl_url_set (h,
+ CURLUPART_URL,
+ url,
+ 0)) ||
+ (CURLUE_OK !=
+ curl_url_get (h,
+ CURLUPART_HOST,
+ &host,
+ 0)) )
+ {
+ /* PH_target_server_base_url passed TALER_is_web_url() at
+ startup, so failing to parse it here would be a bug. */
+ GNUNET_break (0);
+ curl_url_cleanup (h);
return NULL;
}
- hostname += 3;
- end = strchrnul (hostname, '/');
- *end = '\0';
+ /* Absent unless the URL spells the port out; libcurl elides the
+ scheme's default, which is what an origin expects to see. */
+ if (CURLUE_OK !=
+ curl_url_get (h,
+ CURLUPART_PORT,
+ &port,
+ 0))
+ port = NULL;
GNUNET_asprintf (&header,
- "Host: %s",
- hostname);
- GNUNET_free (dup);
+ "Host: %s%s%s",
+ host,
+ (NULL != port) ? ":" : "",
+ (NULL != port) ? port : "");
+ curl_free (host);
+ curl_free (port);
+ curl_url_cleanup (h);
return header;
}
diff --git a/src/tests/README b/src/tests/README
@@ -85,6 +85,17 @@ Per-upstream battery (`run_battery`):
the round-trip
GET /echo-headers paivana adds the reverse-proxy headers
X-Forwarded-For, X-Forwarded-Proto, Via
+ Host: rewritten the Host the upstream sees is the
+ authority of DESTINATION_BASE_URL, not
+ the one the client dialed, and is
+ host[:port] and nothing else (RFC 9110
+ §7.2 — no userinfo, no path, no query).
+ Note this only covers the destination
+ URLs paivana will actually accept:
+ TALER_is_web_url() rejects userinfo and
+ IPv6-literal DESTINATION_BASE_URLs at
+ startup, so those cannot be reached
+ from the driver.
custom X-Test header arbitrary client request headers are
forwarded unchanged
X-Upstream response header upstream response headers survive the
diff --git a/src/tests/test_reverse_proxy.sh b/src/tests/test_reverse_proxy.sh
@@ -98,6 +98,10 @@ export PAIVANA_BASE_CONFIG="$BASE_CONFIG_DIR"
PIDS=()
PAIVANA_PID=""
+# DESTINATION_BASE_URL the running paivana was configured with; the
+# battery derives from it the Host header paivana should be sending
+# upstream.
+PAIVANA_DEST=""
function dump_logs() {
echo "-- logs in $LOGDIR --" >&2
@@ -171,6 +175,7 @@ function start_paivana() {
# than a single "$flags" string) is what keeps a caller that
# passes no extra flags from handing paivana an empty argument.
local dest="$1"; shift
+ PAIVANA_DEST="$dest"
local cfg="$TMPDIR/paivana.conf"
sed -e "s|@DEST@|$dest|g" -e "s|@PORT@|$PAIVANA_PORT|g" \
"$SRCDIR/test_reverse_proxy.conf.in" > "$cfg"
@@ -360,6 +365,22 @@ function run_battery() {
fail "upstream did not see Via: paivana"
ok
+ # RFC 9110 §7.2: the Host we send upstream names the *upstream*
+ # authority, not the one the client dialed, and carries nothing
+ # but host[:port] — no userinfo, no path, no query. `build_host_header`
+ # derives it from DESTINATION_BASE_URL.
+ msg "[$label] Host header sent upstream names the upstream authority"
+ local want_host stripped seen_host
+ stripped="${PAIVANA_DEST#*://}" # drop scheme
+ want_host="${stripped%%/*}" # drop any path
+ seen_host="$(grep -i '^host:' "$TMPDIR/body" | tr -d '\r' | \
+ sed -e 's/^[Hh][Oo][Ss][Tt]: *//')"
+ [ -n "$seen_host" ] || \
+ fail "upstream saw no Host header; headers:\n$(cat "$TMPDIR/body")"
+ [ "$seen_host" = "$want_host" ] || \
+ fail "upstream saw 'Host: $seen_host', want 'Host: $want_host'"
+ ok
+
# RFC 9110 §7.6.3: client's Via chain must be preserved and our
# pseudonym *appended* to it, not replaced.
msg "[$label] client Via is preserved and paivana is appended"