mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-10-25 23:59:59 +00:00
* 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>
332 lines
9.4 KiB
C++
332 lines
9.4 KiB
C++
#pragma once
|
|
|
|
#include "log.h"
|
|
|
|
#include <cpp-httplib/httplib.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
struct common_http_url {
|
|
std::string scheme;
|
|
std::string user;
|
|
std::string password;
|
|
std::string host;
|
|
int port;
|
|
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("://");
|
|
|
|
if (scheme_end == std::string::npos) {
|
|
throw std::runtime_error("invalid URL: no scheme");
|
|
}
|
|
parts.scheme = url.substr(0, scheme_end);
|
|
|
|
if (parts.scheme != "http" && parts.scheme != "https") {
|
|
throw std::runtime_error("unsupported URL scheme: " + parts.scheme);
|
|
}
|
|
|
|
auto rest = url.substr(scheme_end + 3);
|
|
auto at_pos = rest.find('@');
|
|
|
|
if (at_pos != std::string::npos) {
|
|
auto auth = rest.substr(0, at_pos);
|
|
auto colon_pos = auth.find(':');
|
|
if (colon_pos != std::string::npos) {
|
|
parts.user = auth.substr(0, colon_pos);
|
|
parts.password = auth.substr(colon_pos + 1);
|
|
} else {
|
|
parts.user = auth;
|
|
}
|
|
rest = rest.substr(at_pos + 1);
|
|
}
|
|
|
|
auto slash_pos = rest.find('/');
|
|
|
|
if (slash_pos != std::string::npos) {
|
|
parts.host = rest.substr(0, slash_pos);
|
|
parts.path = rest.substr(slash_pos);
|
|
} else {
|
|
parts.host = rest;
|
|
parts.path = "/";
|
|
}
|
|
|
|
// 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 (!port_str.empty()) {
|
|
parts.port = std::stoi(port_str);
|
|
} else if (parts.scheme == "http") {
|
|
parts.port = 80;
|
|
} else if (parts.scheme == "https") {
|
|
parts.port = 443;
|
|
} else {
|
|
throw std::runtime_error("unsupported URL scheme: " + parts.scheme);
|
|
}
|
|
|
|
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);
|
|
|
|
if (parts.host.empty()) {
|
|
throw std::runtime_error("error: invalid URL format");
|
|
}
|
|
|
|
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
if (parts.scheme == "https") {
|
|
throw std::runtime_error(
|
|
"HTTPS is not supported. Please rebuild with one of:\n"
|
|
" -DLLAMA_BUILD_BORINGSSL=ON\n"
|
|
" -DLLAMA_BUILD_LIBRESSL=ON\n"
|
|
" -DLLAMA_OPENSSL=ON (default, requires OpenSSL dev files installed)"
|
|
);
|
|
}
|
|
#endif
|
|
|
|
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);
|
|
}
|
|
|
|
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) };
|
|
}
|
|
|
|
static std::string common_http_show_masked_url(const common_http_url & parts) {
|
|
return parts.scheme + "://" + (parts.user.empty() ? "" : "****:****@") + common_http_format_host(parts.host) + parts.path;
|
|
}
|
|
|
|
static int common_http_get_free_port() {
|
|
#ifdef _WIN32
|
|
WSADATA wsaData;
|
|
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
|
return -1;
|
|
}
|
|
typedef SOCKET native_socket_t;
|
|
#define INVALID_SOCKET_VAL INVALID_SOCKET
|
|
#define CLOSE_SOCKET(s) closesocket(s)
|
|
#else
|
|
typedef int native_socket_t;
|
|
#define INVALID_SOCKET_VAL -1
|
|
#define CLOSE_SOCKET(s) close(s)
|
|
#endif
|
|
|
|
native_socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (sock == INVALID_SOCKET_VAL) {
|
|
#ifdef _WIN32
|
|
WSACleanup();
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
struct sockaddr_in serv_addr;
|
|
std::memset(&serv_addr, 0, sizeof(serv_addr));
|
|
serv_addr.sin_family = AF_INET;
|
|
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
serv_addr.sin_port = htons(0);
|
|
|
|
if (bind(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0) {
|
|
CLOSE_SOCKET(sock);
|
|
#ifdef _WIN32
|
|
WSACleanup();
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
int namelen = sizeof(serv_addr);
|
|
#else
|
|
socklen_t namelen = sizeof(serv_addr);
|
|
#endif
|
|
if (getsockname(sock, (struct sockaddr*)&serv_addr, &namelen) != 0) {
|
|
CLOSE_SOCKET(sock);
|
|
#ifdef _WIN32
|
|
WSACleanup();
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
int port = ntohs(serv_addr.sin_port);
|
|
|
|
CLOSE_SOCKET(sock);
|
|
#ifdef _WIN32
|
|
WSACleanup();
|
|
#endif
|
|
|
|
return port;
|
|
}
|