diff --git a/common/http.h b/common/http.h index 878ad1ce2..10d017f17 100644 --- a/common/http.h +++ b/common/http.h @@ -1,5 +1,7 @@ #pragma once +#include "log.h" + #include #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 common_http_client(const std::string & url) { common_http_url parts = common_http_parse_url(url); @@ -123,6 +237,33 @@ static std::pair 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) }; } diff --git a/tools/server/README.md b/tools/server/README.md index 4e4472ec2..7d697633e 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -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