Просмотр исходного кода

fix(fetcher): allow connecting to a proxy set through the environment

The private-network check refuses to dial a proxy configured through
HTTP_PROXY or HTTPS_PROXY, even though the transport still routes requests
to it. A proxy on a loopback or LAN address is therefore enabled and then
blocked, and the fetch fails with:

  proxyconnect tcp: dial tcp 127.0.0.1:8888:
  fetcher: refusing to access private network host "127.0.0.1"

The trusted-hop exemption covers the feed proxy, the application proxy and
the proxy rotator, but the environment variables were not considered, so
proxyDialAddress stayed empty and the blocking dialer was used for the
connection to the proxy itself.

The proxy for the request is now resolved with x/net/http/httpproxy rather
than http.ProxyFromEnvironment, because the latter caches the environment
on first use and needs a request that does not exist yet at that point.
Reading it directly also keeps the proxy used for routing and the proxy
exempted from the check in agreement, and leaves NO_PROXY evaluated per
request URL, so a host excluded from the proxy is still checked.

x/net is already a direct dependency, so nothing new is required.
Aditya Raj Singh 1 месяц назад
Родитель
Сommit
cedbb39492

+ 17 - 1
internal/reader/fetcher/request_builder.go

@@ -21,6 +21,8 @@ import (
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/proxyrotator"
 	"miniflux.app/v2/internal/urllib"
+
+	"golang.org/x/net/http/httpproxy"
 )
 
 const (
@@ -175,7 +177,19 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
 		KeepAlive: 15 * time.Second, // Default is 30s.
 	}
 
+	// http.ProxyFromEnvironment caches the environment on first use and needs a
+	// request that does not exist yet here, so read the variables directly. This
+	// keeps routing and the private-network exemption below in agreement.
+	envProxyFunc := httpproxy.FromEnvironment().ProxyFunc()
+
 	proxyDialAddress := normalizeProxyDialAddress(clientProxyURL)
+	if clientProxyURL == nil {
+		if parsedRequestURL, err := url.Parse(requestURL); err == nil {
+			if envProxyURL, err := envProxyFunc(parsedRequestURL); err == nil {
+				proxyDialAddress = normalizeProxyDialAddress(envProxyURL)
+			}
+		}
+	}
 
 	// Perform the private-network check inside the dialer's Control callback,
 	// which fires after DNS resolution but before the TCP connection is made.
@@ -199,7 +213,9 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
 	}
 
 	transport := &http.Transport{
-		Proxy: http.ProxyFromEnvironment,
+		Proxy: func(req *http.Request) (*url.URL, error) {
+			return envProxyFunc(req.URL)
+		},
 		// Setting `DialContext` disables HTTP/2, this option forces the transport to try HTTP/2 regardless.
 		ForceAttemptHTTP2: true,
 		MaxIdleConns:      50,               // Default is 100.

+ 67 - 0
internal/reader/fetcher/request_builder_test.go

@@ -534,6 +534,73 @@ func TestRequestBuilder_AllowPrivateConfiguredProxy(t *testing.T) {
 	}
 }
 
+func TestRequestBuilder_AllowPrivateEnvironmentProxy(t *testing.T) {
+	configureFetcherAllowPrivateNetworksOption(t, "0")
+
+	targetURL := "http://feed.invalid/rss.xml"
+	proxyRequests := make(chan string, 1)
+	proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		select {
+		case proxyRequests <- r.URL.String():
+		default:
+		}
+
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer proxyServer.Close()
+
+	t.Setenv("HTTP_PROXY", proxyServer.URL)
+
+	resp, err := NewRequestBuilder().ExecuteRequest(targetURL)
+	if err != nil {
+		t.Fatalf("Expected request through the environment proxy to succeed: %v", err)
+	}
+	defer resp.Body.Close()
+
+	select {
+	case gotURL := <-proxyRequests:
+		if gotURL != targetURL {
+			t.Fatalf("Expected proxy request URL to be %q, got %q", targetURL, gotURL)
+		}
+	default:
+		t.Fatal("Expected request to be sent through the environment proxy")
+	}
+}
+
+func TestRequestBuilder_RefusePrivateNetworkWhenExcludedFromEnvironmentProxy(t *testing.T) {
+	configureFetcherAllowPrivateNetworksOption(t, "0")
+
+	privateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer privateServer.Close()
+
+	proxyRequests := make(chan string, 1)
+	proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		select {
+		case proxyRequests <- r.URL.String():
+		default:
+		}
+
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer proxyServer.Close()
+
+	t.Setenv("HTTP_PROXY", proxyServer.URL)
+	t.Setenv("NO_PROXY", "feed.invalid")
+
+	_, err := NewRequestBuilder().ExecuteRequest("http://feed.invalid/rss.xml")
+	if err == nil {
+		t.Fatal("Expected request excluded from the environment proxy to fail")
+	}
+
+	select {
+	case gotURL := <-proxyRequests:
+		t.Fatalf("Expected request to bypass the environment proxy, but the proxy received %q", gotURL)
+	default:
+	}
+}
+
 func TestRequestBuilder_RefusePrivateNetworkOnRedirect(t *testing.T) {
 	configureFetcherAllowPrivateNetworksOption(t, "0")