common: honor HTTP_PROXY/HTTPS_PROXY env vars in http client (#21752)
* common: honor HTTP_PROXY/HTTPS_PROXY env vars in http client common_http_client() was ignoring standard proxy env vars, causing downloads via -hf and similar flags to fail silently in environments behind an HTTP proxy (corporate networks, air-gapped infra, etc.). Checks HTTPS_PROXY/https_proxy for https:// URLs and HTTP_PROXY/http_proxy for http:// URLs, in that order. If a proxy URL is set and parseable, passes it to httplib's set_proxy() and set_proxy_basic_auth() as appropriate. Malformed proxy URLs are silently ignored so existing callers aren't affected. Fixes #21694 * common: drop misleading comment about generic proxy fallback The comment referenced a 'generic one' that doesn't exist in the code — both HTTP_PROXY and HTTPS_PROXY are scheme-specific. Reviewer noted the mismatch. Addresses review feedback from @ServeurpersoCom. * common : warn when the proxy URL from the environment is malformed * common : honor NO_PROXY when selecting the http proxy * ci : retrigger checks (test-thread-safety flake) * common : bypass the proxy for loopback destinations llama cli talks to its spawned server on 127.0.0.1 through common_http_client, so with HTTP_PROXY set and no NO_PROXY the health polling went to the corporate proxy, which rejects loopback targets, and the wait loop never exits ("Loading model..." hangs forever). A remote proxy can never reach the client machine's own loopback, so these destinations are now always contacted directly, before NO_PROXY is consulted: localhost, *.localhost, 127.0.0.0/8, ::1 and 0.0.0.0. Similar to Go's net/http. Proxy handling for downloads and remote --server-base endpoints is unchanged. Signed-off-by: texasich <101962694+texasich@users.noreply.github.com> * docs: move proxy tip from README to server docs per review: the top level README stays lean; the proxy howto now lives with the server docs under Usage Signed-off-by: texasich <101962694+texasich@users.noreply.github.com> --------- Signed-off-by: texasich <101962694+texasich@users.noreply.github.com> Co-authored-by: texasich <texasich@users.noreply.github.com> Co-authored-by: texasich <texasich@gmail.com> Co-authored-by: marecabo <23156476+marecabo@users.noreply.github.com>
This commit is contained in:
2 files changed
+145
No files matched your search
+141
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "log.h"
|
||||
|
||||
#include <cpp-httplib/httplib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -97,6 +99,118 @@ static common_http_url common_http_parse_url(const std::string & url) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
// match a loopback destination: a remote proxy can never reach the client machine's own
|
||||
// loopback, so these hosts are always contacted directly, before NO_PROXY is consulted
|
||||
// (similar to Go's net/http)
|
||||
static bool common_http_is_loopback(const std::string & host) {
|
||||
auto lower = [](std::string s) {
|
||||
for (auto & c : s) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const std::string h = lower(host);
|
||||
|
||||
if (h == "localhost" || h == "::1" || h == "0.0.0.0") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (h.size() > 10 && h.compare(h.size() - 10, 10, ".localhost") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4 literal: exactly four all-digit octets with values 0-255, loopback is 127.0.0.0/8
|
||||
int octets[4] = { 0, 0, 0, 0 };
|
||||
size_t pos = 0;
|
||||
int n = 0;
|
||||
bool complete = false;
|
||||
while (n < 4 && pos <= h.size()) {
|
||||
size_t dot = h.find('.', pos);
|
||||
if (dot == std::string::npos) {
|
||||
dot = h.size();
|
||||
}
|
||||
if (dot == pos || dot - pos > 3) {
|
||||
return false;
|
||||
}
|
||||
int val = 0;
|
||||
for (size_t i = pos; i < dot; i++) {
|
||||
if (h[i] < '0' || h[i] > '9') {
|
||||
return false;
|
||||
}
|
||||
val = val * 10 + (h[i] - '0');
|
||||
}
|
||||
if (val > 255) {
|
||||
return false;
|
||||
}
|
||||
octets[n++] = val;
|
||||
if (dot == h.size()) {
|
||||
complete = (n == 4);
|
||||
break;
|
||||
}
|
||||
pos = dot + 1;
|
||||
}
|
||||
|
||||
return complete && octets[0] == 127;
|
||||
}
|
||||
|
||||
// match a host against a NO_PROXY list: comma separated entries, "*" bypasses the proxy for every host,
|
||||
// an entry matches the host itself or any of its subdomains, a leading dot is optional, ports are ignored
|
||||
static bool common_http_proxy_bypass(const std::string & no_proxy, const std::string & host) {
|
||||
auto lower = [](std::string s) {
|
||||
for (auto & c : s) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const std::string h = lower(host);
|
||||
|
||||
for (size_t pos = 0; pos < no_proxy.size(); ) {
|
||||
size_t end = no_proxy.find(',', pos);
|
||||
if (end == std::string::npos) {
|
||||
end = no_proxy.size();
|
||||
}
|
||||
|
||||
std::string entry = lower(no_proxy.substr(pos, end - pos));
|
||||
pos = end + 1;
|
||||
|
||||
entry.erase(0, entry.find_first_not_of(" \t"));
|
||||
auto last = entry.find_last_not_of(" \t");
|
||||
if (last == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
entry.erase(last + 1);
|
||||
|
||||
if (entry == "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.front() == '.') {
|
||||
entry.erase(0, 1);
|
||||
}
|
||||
|
||||
if (entry.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (h == entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (h.size() > entry.size() && h[h.size() - entry.size() - 1] == '.' &&
|
||||
h.compare(h.size() - entry.size(), entry.size(), entry) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::pair<httplib::Client, common_http_url> common_http_client(const std::string & url) {
|
||||
common_http_url parts = common_http_parse_url(url);
|
||||
|
||||
@@ -123,6 +237,33 @@ static std::pair<httplib::Client, common_http_url> common_http_client(const std:
|
||||
|
||||
cli.set_follow_location(true);
|
||||
|
||||
// Honor HTTP_PROXY / HTTPS_PROXY environment variables (both lower- and upper-case).
|
||||
auto getenv_s = [](const char * upper, const char * lower) -> std::string {
|
||||
const char * val = std::getenv(upper);
|
||||
if (!val) {
|
||||
val = std::getenv(lower);
|
||||
}
|
||||
return val ? val : "";
|
||||
};
|
||||
|
||||
const std::string proxy_url = (parts.scheme == "https")
|
||||
? getenv_s("HTTPS_PROXY", "https_proxy")
|
||||
: getenv_s("HTTP_PROXY", "http_proxy");
|
||||
|
||||
if (!proxy_url.empty() && !common_http_is_loopback(parts.host) &&
|
||||
!common_http_proxy_bypass(getenv_s("NO_PROXY", "no_proxy"), parts.host)) {
|
||||
try {
|
||||
common_http_url proxy = common_http_parse_url(proxy_url);
|
||||
cli.set_proxy(proxy.host, proxy.port);
|
||||
if (!proxy.user.empty()) {
|
||||
cli.set_proxy_basic_auth(proxy.user, proxy.password);
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
// fall back to a direct connection, the URL itself is not logged as it can carry credentials
|
||||
LOG_WRN("%s: ignoring malformed proxy URL from the environment: %s\n", __func__, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return { std::move(cli), std::move(parts) };
|
||||
}
|
||||
|
||||
|
||||
@@ -405,6 +405,10 @@ Recommended `--cors-origins` setting, depending on where the server runs:
|
||||
|
||||
Related flags: `--cors-origins`, `--cors-methods`, `--cors-headers`, `--cors-credentials` / `--no-cors-credentials`. Background and rationale: [#25655](https://github.com/ggml-org/llama.cpp/pull/25655).
|
||||
|
||||
### Running behind a proxy
|
||||
|
||||
If your host reaches the internet through a corporate proxy, export `HTTP_PROXY` and `HTTPS_PROXY` before running commands that download models (e.g. `export HTTPS_PROXY=http://proxy.example.com:8080`). Hosts listed in `NO_PROXY` and loopback addresses are contacted directly.
|
||||
|
||||
## Build
|
||||
|
||||
`llama-server` is built alongside everything else from the root of the project
|
||||
|
||||
Reference in new issue
Block a user