mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 01:06:27 +00:00
Merge pull request #66516 from tallclair/redirect
Add verification to apiserver redirect following
This commit is contained in:
commit
109b67c291
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user