From 799fcc04a5bbe2823e698b9e524f5a6cd0c594b6 Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 30 Jun 2026 16:16:44 +0200 Subject: [PATCH] common,server: handle bracketed IPv6 literals in URL authority (#25140) * common,server: handle bracketed IPv6 literals in URL authority Parse the [host]:port form (RFC 3986) and bracket IPv6 hosts when formatting a URL authority: listening log, proxy Host header, proxy log, client rebuild. The per-request remote_addr stays bare. * common: restore unsupported scheme throw in url parser Address @ngxson review: keep the explicit reject in port resolution so the block stays self-contained. Non-http(s) schemes still throw (also gated at the top of common_http_parse_url). --- common/http.h | 34 ++++++++++++++++++++++++++------ tools/server/server-cors-proxy.h | 2 +- tools/server/server-http.cpp | 3 ++- tools/server/server-models.cpp | 4 +++- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/common/http.h b/common/http.h index d3daccd6b..e88bc6a5e 100644 --- a/common/http.h +++ b/common/http.h @@ -11,6 +11,11 @@ struct common_http_url { std::string path; }; +// bracket an IPv6 literal host for a URL authority (RFC 3986) +static std::string common_http_format_host(const std::string & host) { + return host.find(':') != std::string::npos ? "[" + host + "]" : host; +} + static common_http_url common_http_parse_url(const std::string & url) { common_http_url parts; auto scheme_end = url.find("://"); @@ -49,11 +54,28 @@ static common_http_url common_http_parse_url(const std::string & url) { parts.path = "/"; } - auto colon_pos = parts.host.find(':'); + // split the authority into host and optional port, a bracketed IPv6 literal keeps its inner colons (RFC 3986) + std::string port_str; + if (!parts.host.empty() && parts.host.front() == '[') { + auto close = parts.host.find(']'); + if (close == std::string::npos) { + throw std::runtime_error("invalid IPv6 URL authority: " + parts.host); + } + auto after = parts.host.substr(close + 1); + if (!after.empty() && after.front() == ':') { + port_str = after.substr(1); + } + parts.host = parts.host.substr(1, close - 1); + } else { + auto colon_pos = parts.host.find(':'); + if (colon_pos != std::string::npos) { + port_str = parts.host.substr(colon_pos + 1); + parts.host = parts.host.substr(0, colon_pos); + } + } - if (colon_pos != std::string::npos) { - parts.port = std::stoi(parts.host.substr(colon_pos + 1)); - parts.host = parts.host.substr(0, colon_pos); + if (!port_str.empty()) { + parts.port = std::stoi(port_str); } else if (parts.scheme == "http") { parts.port = 80; } else if (parts.scheme == "https") { @@ -83,7 +105,7 @@ static std::pair common_http_client(const std: } #endif - httplib::Client cli(parts.scheme + "://" + parts.host + ":" + std::to_string(parts.port)); + httplib::Client cli(parts.scheme + "://" + common_http_format_host(parts.host) + ":" + std::to_string(parts.port)); if (!parts.user.empty()) { cli.set_basic_auth(parts.user, parts.password); @@ -95,5 +117,5 @@ static std::pair common_http_client(const std: } static std::string common_http_show_masked_url(const common_http_url & parts) { - return parts.scheme + "://" + (parts.user.empty() ? "" : "****:****@") + parts.host + parts.path; + return parts.scheme + "://" + (parts.user.empty() ? "" : "****:****@") + common_http_format_host(parts.host) + parts.path; } diff --git a/tools/server/server-cors-proxy.h b/tools/server/server-cors-proxy.h index 53a6909ed..3600a33ec 100644 --- a/tools/server/server-cors-proxy.h +++ b/tools/server/server-cors-proxy.h @@ -39,7 +39,7 @@ static server_http_res_ptr proxy_request(const server_http_req & req, std::strin throw std::runtime_error("unsupported URL scheme in target URL: " + parsed_url.scheme); } - SRV_INF("proxying %s request to %s://%s:%i%s\n", method.c_str(), parsed_url.scheme.c_str(), parsed_url.host.c_str(), parsed_url.port, parsed_url.path.c_str()); + SRV_INF("proxying %s request to %s://%s:%i%s\n", method.c_str(), parsed_url.scheme.c_str(), common_http_format_host(parsed_url.host).c_str(), parsed_url.port, parsed_url.path.c_str()); std::map headers; const std::string proxy_header_prefix = "x-llama-server-proxy-header-"; diff --git a/tools/server/server-http.cpp b/tools/server/server-http.cpp index 21bed64c9..bb88dda21 100644 --- a/tools/server/server-http.cpp +++ b/tools/server/server-http.cpp @@ -1,4 +1,5 @@ #include "common.h" +#include "http.h" #include "server-http.h" #include "server-stream.h" #include "server-common.h" @@ -441,7 +442,7 @@ bool server_http_context::start() { srv->wait_until_ready(); listening_address = is_sock ? string_format("unix://%s", hostname.c_str()) - : string_format("%s://%s:%d", is_ssl ? "https" : "http", hostname.c_str(), port); + : string_format("%s://%s:%d", is_ssl ? "https" : "http", common_http_format_host(hostname).c_str(), port); return true; } diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index 81da00c0e..2ee318f14 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -1,4 +1,5 @@ #include "server-common.h" +#include "http.h" #include "server-models.h" #include "server-context.h" #include "server-stream.h" @@ -2263,7 +2264,8 @@ server_http_proxy::server_http_proxy( } if (lowered == "host") { bool is_default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80); - req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port)); + const std::string url_host = common_http_format_host(host); + req.set_header(key, is_default_port ? url_host : url_host + ":" + std::to_string(port)); } else { req.set_header(key, value); }