mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	Only allow apiserver to follow redriects to the same host
This commit is contained in:
		@@ -51,6 +51,7 @@ import (
 | 
				
			|||||||
	api "k8s.io/kubernetes/pkg/apis/core"
 | 
						api "k8s.io/kubernetes/pkg/apis/core"
 | 
				
			||||||
	runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
 | 
						runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
 | 
				
			||||||
	statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
 | 
						statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Do some initialization to decode the query parameters correctly.
 | 
						// Do some initialization to decode the query parameters correctly.
 | 
				
			||||||
	_ "k8s.io/kubernetes/pkg/apis/core/install"
 | 
						_ "k8s.io/kubernetes/pkg/apis/core/install"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubelet/cm"
 | 
						"k8s.io/kubernetes/pkg/kubelet/cm"
 | 
				
			||||||
@@ -1166,7 +1167,7 @@ func TestServeExecInContainerIdleTimeout(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	url := fw.testHTTPServer.URL + "/exec/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?c=ls&c=-a&" + api.ExecStdinParam + "=1"
 | 
						url := fw.testHTTPServer.URL + "/exec/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?c=ls&c=-a&" + api.ExecStdinParam + "=1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	upgradeRoundTripper := spdy.NewSpdyRoundTripper(nil, true)
 | 
						upgradeRoundTripper := spdy.NewSpdyRoundTripper(nil, true, true)
 | 
				
			||||||
	c := &http.Client{Transport: upgradeRoundTripper}
 | 
						c := &http.Client{Transport: upgradeRoundTripper}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := c.Post(url, "", nil)
 | 
						resp, err := c.Post(url, "", nil)
 | 
				
			||||||
@@ -1332,7 +1333,7 @@ func testExecAttach(t *testing.T, verb string) {
 | 
				
			|||||||
					return http.ErrUseLastResponse
 | 
										return http.ErrUseLastResponse
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				upgradeRoundTripper = spdy.NewRoundTripper(nil, true)
 | 
									upgradeRoundTripper = spdy.NewRoundTripper(nil, true, true)
 | 
				
			||||||
				c = &http.Client{Transport: upgradeRoundTripper}
 | 
									c = &http.Client{Transport: upgradeRoundTripper}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1429,7 +1430,7 @@ func TestServePortForwardIdleTimeout(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	url := fw.testHTTPServer.URL + "/portForward/" + podNamespace + "/" + podName
 | 
						url := fw.testHTTPServer.URL + "/portForward/" + podNamespace + "/" + podName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	upgradeRoundTripper := spdy.NewRoundTripper(nil, true)
 | 
						upgradeRoundTripper := spdy.NewRoundTripper(nil, true, true)
 | 
				
			||||||
	c := &http.Client{Transport: upgradeRoundTripper}
 | 
						c := &http.Client{Transport: upgradeRoundTripper}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := c.Post(url, "", nil)
 | 
						resp, err := c.Post(url, "", nil)
 | 
				
			||||||
@@ -1536,7 +1537,7 @@ func TestServePortForward(t *testing.T) {
 | 
				
			|||||||
					return http.ErrUseLastResponse
 | 
										return http.ErrUseLastResponse
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				upgradeRoundTripper = spdy.NewRoundTripper(nil, true)
 | 
									upgradeRoundTripper = spdy.NewRoundTripper(nil, true, true)
 | 
				
			||||||
				c = &http.Client{Transport: upgradeRoundTripper}
 | 
									c = &http.Client{Transport: upgradeRoundTripper}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,6 +80,7 @@ func (r *LogREST) Get(ctx context.Context, name string, opts runtime.Object) (ru
 | 
				
			|||||||
		ContentType:     "text/plain",
 | 
							ContentType:     "text/plain",
 | 
				
			||||||
		Flush:           logOpts.Follow,
 | 
							Flush:           logOpts.Follow,
 | 
				
			||||||
		ResponseChecker: genericrest.NewGenericHttpResponseChecker(api.Resource("pods/log"), name),
 | 
							ResponseChecker: genericrest.NewGenericHttpResponseChecker(api.Resource("pods/log"), name),
 | 
				
			||||||
 | 
							RedirectChecker: genericrest.PreventRedirects,
 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -194,6 +194,7 @@ func (r *PortForwardREST) Connect(ctx context.Context, name string, opts runtime
 | 
				
			|||||||
func newThrottledUpgradeAwareProxyHandler(location *url.URL, transport http.RoundTripper, wrapTransport, upgradeRequired, interceptRedirects bool, responder rest.Responder) *proxy.UpgradeAwareHandler {
 | 
					func newThrottledUpgradeAwareProxyHandler(location *url.URL, transport http.RoundTripper, wrapTransport, upgradeRequired, interceptRedirects bool, responder rest.Responder) *proxy.UpgradeAwareHandler {
 | 
				
			||||||
	handler := proxy.NewUpgradeAwareHandler(location, transport, wrapTransport, upgradeRequired, proxy.NewErrorResponder(responder))
 | 
						handler := proxy.NewUpgradeAwareHandler(location, transport, wrapTransport, upgradeRequired, proxy.NewErrorResponder(responder))
 | 
				
			||||||
	handler.InterceptRedirects = interceptRedirects && utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects)
 | 
						handler.InterceptRedirects = interceptRedirects && utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects)
 | 
				
			||||||
 | 
						handler.RequireSameHostRedirects = utilfeature.DefaultFeatureGate.Enabled(genericfeatures.ValidateProxyRedirects)
 | 
				
			||||||
	handler.MaxBytesPerSec = capabilities.Get().PerConnectionBandwidthLimitBytesPerSec
 | 
						handler.MaxBytesPerSec = capabilities.Get().PerConnectionBandwidthLimitBytesPerSec
 | 
				
			||||||
	return handler
 | 
						return handler
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,9 @@ type SpdyRoundTripper struct {
 | 
				
			|||||||
	// followRedirects indicates if the round tripper should examine responses for redirects and
 | 
						// followRedirects indicates if the round tripper should examine responses for redirects and
 | 
				
			||||||
	// follow them.
 | 
						// follow them.
 | 
				
			||||||
	followRedirects bool
 | 
						followRedirects bool
 | 
				
			||||||
 | 
						// requireSameHostRedirects restricts redirect following to only follow redirects to the same host
 | 
				
			||||||
 | 
						// as the original request.
 | 
				
			||||||
 | 
						requireSameHostRedirects bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
 | 
					var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
 | 
				
			||||||
@@ -75,14 +78,18 @@ var _ utilnet.Dialer = &SpdyRoundTripper{}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// NewRoundTripper creates a new SpdyRoundTripper that will use
 | 
					// NewRoundTripper creates a new SpdyRoundTripper that will use
 | 
				
			||||||
// the specified tlsConfig.
 | 
					// the specified tlsConfig.
 | 
				
			||||||
func NewRoundTripper(tlsConfig *tls.Config, followRedirects bool) httpstream.UpgradeRoundTripper {
 | 
					func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) httpstream.UpgradeRoundTripper {
 | 
				
			||||||
	return NewSpdyRoundTripper(tlsConfig, followRedirects)
 | 
						return NewSpdyRoundTripper(tlsConfig, followRedirects, requireSameHostRedirects)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewSpdyRoundTripper creates a new SpdyRoundTripper that will use
 | 
					// NewSpdyRoundTripper creates a new SpdyRoundTripper that will use
 | 
				
			||||||
// the specified tlsConfig. This function is mostly meant for unit tests.
 | 
					// the specified tlsConfig. This function is mostly meant for unit tests.
 | 
				
			||||||
func NewSpdyRoundTripper(tlsConfig *tls.Config, followRedirects bool) *SpdyRoundTripper {
 | 
					func NewSpdyRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper {
 | 
				
			||||||
	return &SpdyRoundTripper{tlsConfig: tlsConfig, followRedirects: followRedirects}
 | 
						return &SpdyRoundTripper{
 | 
				
			||||||
 | 
							tlsConfig:                tlsConfig,
 | 
				
			||||||
 | 
							followRedirects:          followRedirects,
 | 
				
			||||||
 | 
							requireSameHostRedirects: requireSameHostRedirects,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
 | 
					// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
 | 
				
			||||||
@@ -257,7 +264,7 @@ func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s.followRedirects {
 | 
						if s.followRedirects {
 | 
				
			||||||
		conn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, req.URL, header, req.Body, s)
 | 
							conn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, req.URL, header, req.Body, s, s.requireSameHostRedirects)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		clone := utilnet.CloneRequest(req)
 | 
							clone := utilnet.CloneRequest(req)
 | 
				
			||||||
		clone.Header = header
 | 
							clone.Header = header
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -282,7 +282,7 @@ func TestRoundTripAndNewConnection(t *testing.T) {
 | 
				
			|||||||
					t.Fatalf("%s: Error creating request: %s", k, err)
 | 
										t.Fatalf("%s: Error creating request: %s", k, err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				spdyTransport := NewSpdyRoundTripper(testCase.clientTLS, redirect)
 | 
									spdyTransport := NewSpdyRoundTripper(testCase.clientTLS, redirect, redirect)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				var proxierCalled bool
 | 
									var proxierCalled bool
 | 
				
			||||||
				var proxyCalledWithHost string
 | 
									var proxyCalledWithHost string
 | 
				
			||||||
@@ -391,8 +391,8 @@ func TestRoundTripRedirects(t *testing.T) {
 | 
				
			|||||||
	}{
 | 
						}{
 | 
				
			||||||
		{0, true},
 | 
							{0, true},
 | 
				
			||||||
		{1, true},
 | 
							{1, true},
 | 
				
			||||||
		{10, true},
 | 
							{9, true},
 | 
				
			||||||
		{11, false},
 | 
							{10, false},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, test := range tests {
 | 
						for _, test := range tests {
 | 
				
			||||||
		t.Run(fmt.Sprintf("with %d redirects", test.redirects), func(t *testing.T) {
 | 
							t.Run(fmt.Sprintf("with %d redirects", test.redirects), func(t *testing.T) {
 | 
				
			||||||
@@ -425,7 +425,7 @@ func TestRoundTripRedirects(t *testing.T) {
 | 
				
			|||||||
				t.Fatalf("Error creating request: %s", err)
 | 
									t.Fatalf("Error creating request: %s", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			spdyTransport := NewSpdyRoundTripper(nil, true)
 | 
								spdyTransport := NewSpdyRoundTripper(nil, true, true)
 | 
				
			||||||
			client := &http.Client{Transport: spdyTransport}
 | 
								client := &http.Client{Transport: spdyTransport}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			resp, err := client.Do(req)
 | 
								resp, err := client.Do(req)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,12 @@ go_test(
 | 
				
			|||||||
        "util_test.go",
 | 
					        "util_test.go",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    embed = [":go_default_library"],
 | 
					    embed = [":go_default_library"],
 | 
				
			||||||
    deps = ["//vendor/github.com/spf13/pflag:go_default_library"],
 | 
					    deps = [
 | 
				
			||||||
 | 
					        "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
 | 
				
			||||||
 | 
					        "//vendor/github.com/spf13/pflag:go_default_library",
 | 
				
			||||||
 | 
					        "//vendor/github.com/stretchr/testify/assert:go_default_library",
 | 
				
			||||||
 | 
					        "//vendor/github.com/stretchr/testify/require:go_default_library",
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go_library(
 | 
					go_library(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -321,9 +321,10 @@ type Dialer interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ConnectWithRedirects uses dialer to send req, following up to 10 redirects (relative to
 | 
					// ConnectWithRedirects uses dialer to send req, following up to 10 redirects (relative to
 | 
				
			||||||
// originalLocation). It returns the opened net.Conn and the raw response bytes.
 | 
					// originalLocation). It returns the opened net.Conn and the raw response bytes.
 | 
				
			||||||
func ConnectWithRedirects(originalMethod string, originalLocation *url.URL, header http.Header, originalBody io.Reader, dialer Dialer) (net.Conn, []byte, error) {
 | 
					// If requireSameHostRedirects is true, only redirects to the same host are permitted.
 | 
				
			||||||
 | 
					func ConnectWithRedirects(originalMethod string, originalLocation *url.URL, header http.Header, originalBody io.Reader, dialer Dialer, requireSameHostRedirects bool) (net.Conn, []byte, error) {
 | 
				
			||||||
	const (
 | 
						const (
 | 
				
			||||||
		maxRedirects    = 10
 | 
							maxRedirects    = 9     // Fail on the 10th redirect
 | 
				
			||||||
		maxResponseSize = 16384 // play it safe to allow the potential for lots of / large headers
 | 
							maxResponseSize = 16384 // play it safe to allow the potential for lots of / large headers
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -387,10 +388,6 @@ redirectLoop:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		resp.Body.Close() // not used
 | 
							resp.Body.Close() // not used
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Reset the connection.
 | 
					 | 
				
			||||||
		intermediateConn.Close()
 | 
					 | 
				
			||||||
		intermediateConn = nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Prepare to follow the redirect.
 | 
							// Prepare to follow the redirect.
 | 
				
			||||||
		redirectStr := resp.Header.Get("Location")
 | 
							redirectStr := resp.Header.Get("Location")
 | 
				
			||||||
		if redirectStr == "" {
 | 
							if redirectStr == "" {
 | 
				
			||||||
@@ -404,6 +401,15 @@ redirectLoop:
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, fmt.Errorf("malformed Location header: %v", err)
 | 
								return nil, nil, fmt.Errorf("malformed Location header: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Only follow redirects to the same host. Otherwise, propagate the redirect response back.
 | 
				
			||||||
 | 
							if requireSameHostRedirects && location.Hostname() != originalLocation.Hostname() {
 | 
				
			||||||
 | 
								break redirectLoop
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Reset the connection.
 | 
				
			||||||
 | 
							intermediateConn.Close()
 | 
				
			||||||
 | 
							intermediateConn = nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	connToReturn := intermediateConn
 | 
						connToReturn := intermediateConn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,14 +19,23 @@ limitations under the License.
 | 
				
			|||||||
package net
 | 
					package net
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/wait"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestGetClientIP(t *testing.T) {
 | 
					func TestGetClientIP(t *testing.T) {
 | 
				
			||||||
@@ -280,3 +289,153 @@ func TestJoinPreservingTrailingSlash(t *testing.T) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestConnectWithRedirects(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							desc              string
 | 
				
			||||||
 | 
							redirects         []string
 | 
				
			||||||
 | 
							method            string // initial request method, empty == GET
 | 
				
			||||||
 | 
							expectError       bool
 | 
				
			||||||
 | 
							expectedRedirects int
 | 
				
			||||||
 | 
							newPort           bool // special case different port test
 | 
				
			||||||
 | 
						}{{
 | 
				
			||||||
 | 
							desc:              "relative redirects allowed",
 | 
				
			||||||
 | 
							redirects:         []string{"/ok"},
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "redirects to the same host are allowed",
 | 
				
			||||||
 | 
							redirects:         []string{"http://HOST/ok"}, // HOST replaced with server address in test
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "POST redirects to GET",
 | 
				
			||||||
 | 
							method:            http.MethodPost,
 | 
				
			||||||
 | 
							redirects:         []string{"/ok"},
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "PUT redirects to GET",
 | 
				
			||||||
 | 
							method:            http.MethodPut,
 | 
				
			||||||
 | 
							redirects:         []string{"/ok"},
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "DELETE redirects to GET",
 | 
				
			||||||
 | 
							method:            http.MethodDelete,
 | 
				
			||||||
 | 
							redirects:         []string{"/ok"},
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "9 redirects are allowed",
 | 
				
			||||||
 | 
							redirects:         []string{"/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9"},
 | 
				
			||||||
 | 
							expectedRedirects: 9,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:        "10 redirects are forbidden",
 | 
				
			||||||
 | 
							redirects:   []string{"/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9", "/10"},
 | 
				
			||||||
 | 
							expectError: true,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "redirect to different host are prevented",
 | 
				
			||||||
 | 
							redirects:         []string{"http://example.com/foo"},
 | 
				
			||||||
 | 
							expectedRedirects: 0,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "multiple redirect to different host forbidden",
 | 
				
			||||||
 | 
							redirects:         []string{"/1", "/2", "/3", "http://example.com/foo"},
 | 
				
			||||||
 | 
							expectedRedirects: 3,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							desc:              "redirect to different port is allowed",
 | 
				
			||||||
 | 
							redirects:         []string{"http://HOST/foo"},
 | 
				
			||||||
 | 
							expectedRedirects: 1,
 | 
				
			||||||
 | 
							newPort:           true,
 | 
				
			||||||
 | 
						}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const resultString = "Test output"
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.desc, func(t *testing.T) {
 | 
				
			||||||
 | 
								redirectCount := 0
 | 
				
			||||||
 | 
								s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
 | 
									// Verify redirect request.
 | 
				
			||||||
 | 
									if redirectCount > 0 {
 | 
				
			||||||
 | 
										expectedURL, err := url.Parse(test.redirects[redirectCount-1])
 | 
				
			||||||
 | 
										require.NoError(t, err, "test URL error")
 | 
				
			||||||
 | 
										assert.Equal(t, req.URL.Path, expectedURL.Path, "unknown redirect path")
 | 
				
			||||||
 | 
										assert.Equal(t, http.MethodGet, req.Method, "redirects must always be GET")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if redirectCount < len(test.redirects) {
 | 
				
			||||||
 | 
										http.Redirect(w, req, test.redirects[redirectCount], http.StatusFound)
 | 
				
			||||||
 | 
										redirectCount++
 | 
				
			||||||
 | 
									} else if redirectCount == len(test.redirects) {
 | 
				
			||||||
 | 
										w.Write([]byte(resultString))
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										t.Errorf("unexpected number of redirects %d to %s", redirectCount, req.URL.String())
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}))
 | 
				
			||||||
 | 
								defer s.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								u, err := url.Parse(s.URL)
 | 
				
			||||||
 | 
								require.NoError(t, err, "Error parsing server URL")
 | 
				
			||||||
 | 
								host := u.Host
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Special case new-port test with a secondary server.
 | 
				
			||||||
 | 
								if test.newPort {
 | 
				
			||||||
 | 
									s2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
 | 
										w.Write([]byte(resultString))
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
									defer s2.Close()
 | 
				
			||||||
 | 
									u2, err := url.Parse(s2.URL)
 | 
				
			||||||
 | 
									require.NoError(t, err, "Error parsing secondary server URL")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Sanity check: secondary server uses same hostname, different port.
 | 
				
			||||||
 | 
									require.Equal(t, u.Hostname(), u2.Hostname(), "sanity check: same hostname")
 | 
				
			||||||
 | 
									require.NotEqual(t, u.Port(), u2.Port(), "sanity check: different port")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Redirect to the secondary server.
 | 
				
			||||||
 | 
									host = u2.Host
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Update redirect URLs with actual host.
 | 
				
			||||||
 | 
								for i := range test.redirects {
 | 
				
			||||||
 | 
									test.redirects[i] = strings.Replace(test.redirects[i], "HOST", host, 1)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								method := test.method
 | 
				
			||||||
 | 
								if method == "" {
 | 
				
			||||||
 | 
									method = http.MethodGet
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								netdialer := &net.Dialer{
 | 
				
			||||||
 | 
									Timeout:   wait.ForeverTestTimeout,
 | 
				
			||||||
 | 
									KeepAlive: wait.ForeverTestTimeout,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								dialer := DialerFunc(func(req *http.Request) (net.Conn, error) {
 | 
				
			||||||
 | 
									conn, err := netdialer.Dial("tcp", req.URL.Host)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return conn, err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if err = req.Write(conn); err != nil {
 | 
				
			||||||
 | 
										require.NoError(t, conn.Close())
 | 
				
			||||||
 | 
										return nil, fmt.Errorf("error sending request: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return conn, err
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								conn, rawResponse, err := ConnectWithRedirects(method, u, http.Header{} /*body*/, nil, dialer, true)
 | 
				
			||||||
 | 
								if test.expectError {
 | 
				
			||||||
 | 
									require.Error(t, err, "expected request error")
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								require.NoError(t, err, "unexpected request error")
 | 
				
			||||||
 | 
								assert.NoError(t, conn.Close(), "error closing connection")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(rawResponse)), nil)
 | 
				
			||||||
 | 
								require.NoError(t, err, "unexpected request error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								result, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
								require.NoError(t, resp.Body.Close())
 | 
				
			||||||
 | 
								if test.expectedRedirects < len(test.redirects) {
 | 
				
			||||||
 | 
									// Expect the last redirect to be returned.
 | 
				
			||||||
 | 
									assert.Equal(t, http.StatusFound, resp.StatusCode, "Final response is not a redirect")
 | 
				
			||||||
 | 
									assert.Equal(t, test.redirects[len(test.redirects)-1], resp.Header.Get("Location"))
 | 
				
			||||||
 | 
									assert.NotEqual(t, resultString, string(result), "wrong content")
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									assert.Equal(t, resultString, string(result), "stream content does not match")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,6 +68,8 @@ type UpgradeAwareHandler struct {
 | 
				
			|||||||
	// InterceptRedirects determines whether the proxy should sniff backend responses for redirects,
 | 
						// InterceptRedirects determines whether the proxy should sniff backend responses for redirects,
 | 
				
			||||||
	// following them as necessary.
 | 
						// following them as necessary.
 | 
				
			||||||
	InterceptRedirects bool
 | 
						InterceptRedirects bool
 | 
				
			||||||
 | 
						// RequireSameHostRedirects only allows redirects to the same host. It is only used if InterceptRedirects=true.
 | 
				
			||||||
 | 
						RequireSameHostRedirects bool
 | 
				
			||||||
	// UseRequestLocation will use the incoming request URL when talking to the backend server.
 | 
						// UseRequestLocation will use the incoming request URL when talking to the backend server.
 | 
				
			||||||
	UseRequestLocation bool
 | 
						UseRequestLocation bool
 | 
				
			||||||
	// FlushInterval controls how often the standard HTTP proxy will flush content from the upstream.
 | 
						// FlushInterval controls how often the standard HTTP proxy will flush content from the upstream.
 | 
				
			||||||
@@ -256,7 +258,7 @@ func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Reques
 | 
				
			|||||||
	utilnet.AppendForwardedForHeader(clone)
 | 
						utilnet.AppendForwardedForHeader(clone)
 | 
				
			||||||
	if h.InterceptRedirects {
 | 
						if h.InterceptRedirects {
 | 
				
			||||||
		glog.V(6).Infof("Connecting to backend proxy (intercepting redirects) %s\n  Headers: %v", &location, clone.Header)
 | 
							glog.V(6).Infof("Connecting to backend proxy (intercepting redirects) %s\n  Headers: %v", &location, clone.Header)
 | 
				
			||||||
		backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade))
 | 
							backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade), h.RequireSameHostRedirects)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		glog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n  Headers: %v", &location, clone.Header)
 | 
							glog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n  Headers: %v", &location, clone.Header)
 | 
				
			||||||
		clone.URL = &location
 | 
							clone.URL = &location
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,11 +29,19 @@ const (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// owner: @tallclair
 | 
						// owner: @tallclair
 | 
				
			||||||
	// alpha: v1.5
 | 
						// alpha: v1.5
 | 
				
			||||||
 | 
						// beta: v1.6
 | 
				
			||||||
	//
 | 
						//
 | 
				
			||||||
	// StreamingProxyRedirects controls whether the apiserver should intercept (and follow)
 | 
						// StreamingProxyRedirects controls whether the apiserver should intercept (and follow)
 | 
				
			||||||
	// redirects from the backend (Kubelet) for streaming requests (exec/attach/port-forward).
 | 
						// redirects from the backend (Kubelet) for streaming requests (exec/attach/port-forward).
 | 
				
			||||||
	StreamingProxyRedirects utilfeature.Feature = "StreamingProxyRedirects"
 | 
						StreamingProxyRedirects utilfeature.Feature = "StreamingProxyRedirects"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// owner: @tallclair
 | 
				
			||||||
 | 
						// alpha: v1.10
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// ValidateProxyRedirects controls whether the apiserver should validate that redirects are only
 | 
				
			||||||
 | 
						// followed to the same host. Only used if StreamingProxyRedirects is enabled.
 | 
				
			||||||
 | 
						ValidateProxyRedirects utilfeature.Feature = "ValidateProxyRedirects"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// owner: @tallclair
 | 
						// owner: @tallclair
 | 
				
			||||||
	// alpha: v1.7
 | 
						// alpha: v1.7
 | 
				
			||||||
	// beta: v1.8
 | 
						// beta: v1.8
 | 
				
			||||||
@@ -83,6 +91,7 @@ func init() {
 | 
				
			|||||||
// available throughout Kubernetes binaries.
 | 
					// available throughout Kubernetes binaries.
 | 
				
			||||||
var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
 | 
					var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
 | 
				
			||||||
	StreamingProxyRedirects: {Default: true, PreRelease: utilfeature.Beta},
 | 
						StreamingProxyRedirects: {Default: true, PreRelease: utilfeature.Beta},
 | 
				
			||||||
 | 
						ValidateProxyRedirects:  {Default: false, PreRelease: utilfeature.Alpha},
 | 
				
			||||||
	AdvancedAuditing:        {Default: true, PreRelease: utilfeature.GA},
 | 
						AdvancedAuditing:        {Default: true, PreRelease: utilfeature.GA},
 | 
				
			||||||
	APIResponseCompression:  {Default: false, PreRelease: utilfeature.Alpha},
 | 
						APIResponseCompression:  {Default: false, PreRelease: utilfeature.Alpha},
 | 
				
			||||||
	Initializers:            {Default: false, PreRelease: utilfeature.Alpha},
 | 
						Initializers:            {Default: false, PreRelease: utilfeature.Alpha},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,8 @@ go_test(
 | 
				
			|||||||
        "//staging/src/k8s.io/api/core/v1:go_default_library",
 | 
					        "//staging/src/k8s.io/api/core/v1:go_default_library",
 | 
				
			||||||
        "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
 | 
					        "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
 | 
				
			||||||
        "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
 | 
					        "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
 | 
				
			||||||
 | 
					        "//vendor/github.com/stretchr/testify/assert:go_default_library",
 | 
				
			||||||
 | 
					        "//vendor/github.com/stretchr/testify/require:go_default_library",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ package rest
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
@@ -29,13 +30,14 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LocationStreamer is a resource that streams the contents of a particular
 | 
					// LocationStreamer is a resource that streams the contents of a particular
 | 
				
			||||||
// location URL
 | 
					// location URL.
 | 
				
			||||||
type LocationStreamer struct {
 | 
					type LocationStreamer struct {
 | 
				
			||||||
	Location        *url.URL
 | 
						Location        *url.URL
 | 
				
			||||||
	Transport       http.RoundTripper
 | 
						Transport       http.RoundTripper
 | 
				
			||||||
	ContentType     string
 | 
						ContentType     string
 | 
				
			||||||
	Flush           bool
 | 
						Flush           bool
 | 
				
			||||||
	ResponseChecker HttpResponseChecker
 | 
						ResponseChecker HttpResponseChecker
 | 
				
			||||||
 | 
						RedirectChecker func(req *http.Request, via []*http.Request) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// a LocationStreamer must implement a rest.ResourceStreamer
 | 
					// a LocationStreamer must implement a rest.ResourceStreamer
 | 
				
			||||||
@@ -59,7 +61,10 @@ func (s *LocationStreamer) InputStream(ctx context.Context, apiVersion, acceptHe
 | 
				
			|||||||
	if transport == nil {
 | 
						if transport == nil {
 | 
				
			||||||
		transport = http.DefaultTransport
 | 
							transport = http.DefaultTransport
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	client := &http.Client{Transport: transport}
 | 
						client := &http.Client{
 | 
				
			||||||
 | 
							Transport:     transport,
 | 
				
			||||||
 | 
							CheckRedirect: s.RedirectChecker,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	req, err := http.NewRequest("GET", s.Location.String(), nil)
 | 
						req, err := http.NewRequest("GET", s.Location.String(), nil)
 | 
				
			||||||
	// Pass the parent context down to the request to ensure that the resources
 | 
						// Pass the parent context down to the request to ensure that the resources
 | 
				
			||||||
	// will be release properly.
 | 
						// will be release properly.
 | 
				
			||||||
@@ -87,3 +92,8 @@ func (s *LocationStreamer) InputStream(ctx context.Context, apiVersion, acceptHe
 | 
				
			|||||||
	stream = resp.Body
 | 
						stream = resp.Body
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PreventRedirects is a redirect checker that prevents the client from following a redirect.
 | 
				
			||||||
 | 
					func PreventRedirects(_ *http.Request, _ []*http.Request) error {
 | 
				
			||||||
 | 
						return errors.New("redirects forbidden")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,8 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/errors"
 | 
						"k8s.io/apimachinery/pkg/api/errors"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -147,3 +149,23 @@ func TestInputStreamInternalServerErrorTransport(t *testing.T) {
 | 
				
			|||||||
		t.Errorf("StreamInternalServerError does not match. Got: %s. Expected: %s.", err, expectedError)
 | 
							t.Errorf("StreamInternalServerError does not match. Got: %s. Expected: %s.", err, expectedError)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestInputStreamRedirects(t *testing.T) {
 | 
				
			||||||
 | 
						const redirectPath = "/redirect"
 | 
				
			||||||
 | 
						s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
 | 
							if req.URL.Path == redirectPath {
 | 
				
			||||||
 | 
								t.Fatal("Redirects should not be followed")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								http.Redirect(w, req, redirectPath, http.StatusFound)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
						loc, err := url.Parse(s.URL)
 | 
				
			||||||
 | 
						require.NoError(t, err, "Error parsing server URL")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						streamer := &LocationStreamer{
 | 
				
			||||||
 | 
							Location:        loc,
 | 
				
			||||||
 | 
							RedirectChecker: PreventRedirects,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, _, _, err = streamer.InputStream(context.Background(), "", "")
 | 
				
			||||||
 | 
						assert.Error(t, err, "Redirect should trigger an error")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ func RoundTripperFor(config *restclient.Config) (http.RoundTripper, Upgrader, er
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, true)
 | 
						upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, true, false)
 | 
				
			||||||
	wrapper, err := restclient.HTTPWrappersForConfig(config, upgradeRoundTripper)
 | 
						wrapper, err := restclient.HTTPWrappersForConfig(config, upgradeRoundTripper)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -161,7 +161,8 @@ func maybeWrapForConnectionUpgrades(restConfig *restclient.Config, rt http.Round
 | 
				
			|||||||
		return nil, true, err
 | 
							return nil, true, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	followRedirects := utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects)
 | 
						followRedirects := utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects)
 | 
				
			||||||
	upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, followRedirects)
 | 
						requireSameHostRedirects := utilfeature.DefaultFeatureGate.Enabled(genericfeatures.ValidateProxyRedirects)
 | 
				
			||||||
 | 
						upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, followRedirects, requireSameHostRedirects)
 | 
				
			||||||
	wrappedRT, err := restclient.HTTPWrappersForConfig(restConfig, upgradeRoundTripper)
 | 
						wrappedRT, err := restclient.HTTPWrappersForConfig(restConfig, upgradeRoundTripper)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, true, err
 | 
							return nil, true, err
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user