Preserve leading and trailing slashes on proxy subpaths

pull/6/head
Jordan Liggitt 2017-09-22 22:35:20 -04:00
parent 158f6b78da
commit 04eede9b2a
No known key found for this signature in database
GPG Key ID: 39928704103C7229
9 changed files with 97 additions and 13 deletions

View File

@ -14,6 +14,7 @@ go_library(
"//pkg/kubelet/client:go_default_library",
"//pkg/registry/core/node:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/proxy:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",

View File

@ -20,9 +20,9 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/proxy"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
@ -70,7 +70,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
if err != nil {
return nil, err
}
location.Path = path.Join("/", location.Path, proxyOpts.Path)
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
}

View File

@ -20,6 +20,7 @@ go_library(
"//pkg/registry/core/pod:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/proxy:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",

View File

@ -20,9 +20,9 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/proxy"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
@ -71,7 +71,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
if err != nil {
return nil, err
}
location.Path = path.Join("/", location.Path, proxyOpts.Path)
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, false, responder), nil
}

View File

@ -20,9 +20,9 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/proxy"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
@ -66,7 +66,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
if err != nil {
return nil, err
}
location.Path = path.Join("/", location.Path, proxyOpts.Path)
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
}

View File

@ -26,6 +26,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
@ -33,6 +34,26 @@ import (
"golang.org/x/net/http2"
)
// JoinPreservingTrailingSlash does a path.Join of the specified elements,
// preserving any trailing slash on the last non-empty segment
func JoinPreservingTrailingSlash(elem ...string) string {
// do the basic path join
result := path.Join(elem...)
// find the last non-empty segment
for i := len(elem) - 1; i >= 0; i-- {
if len(elem[i]) > 0 {
// if the last segment ended in a slash, ensure our result does as well
if strings.HasSuffix(elem[i], "/") && !strings.HasSuffix(result, "/") {
result += "/"
}
break
}
}
return result
}
// IsProbableEOF returns true if the given error resembles a connection termination
// scenario that would justify assuming that the watch is empty.
// These errors are what the Go http stack returns back to us which are general

View File

@ -20,6 +20,7 @@ package net
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
@ -218,3 +219,40 @@ func TestTLSClientConfigHolder(t *testing.T) {
t.Errorf("didn't find tls config")
}
}
func TestJoinPreservingTrailingSlash(t *testing.T) {
tests := []struct {
a string
b string
want string
}{
// All empty
{"", "", ""},
// Empty a
{"", "/", "/"},
{"", "foo", "foo"},
{"", "/foo", "/foo"},
{"", "/foo/", "/foo/"},
// Empty b
{"/", "", "/"},
{"foo", "", "foo"},
{"/foo", "", "/foo"},
{"/foo/", "", "/foo/"},
// Both populated
{"/", "/", "/"},
{"foo", "foo", "foo/foo"},
{"/foo", "/foo", "/foo/foo"},
{"/foo/", "/foo/", "/foo/foo/"},
}
for _, tt := range tests {
name := fmt.Sprintf("%q+%q=%q", tt.a, tt.b, tt.want)
t.Run(name, func(t *testing.T) {
if got := JoinPreservingTrailingSlash(tt.a, tt.b); got != tt.want {
t.Errorf("JoinPreservingTrailingSlash() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -2135,15 +2135,25 @@ func TestGetWithOptions(t *testing.T) {
requestURL: "/namespaces/default/simple/id?param1=test1&param2=test2",
expectedPath: "",
},
{
name: "with root slash",
requestURL: "/namespaces/default/simple/id/?param1=test1&param2=test2",
expectedPath: "/",
},
{
name: "with path",
requestURL: "/namespaces/default/simple/id/a/different/path?param1=test1&param2=test2",
expectedPath: "a/different/path",
expectedPath: "/a/different/path",
},
{
name: "with path with trailing slash",
requestURL: "/namespaces/default/simple/id/a/different/path/?param1=test1&param2=test2",
expectedPath: "/a/different/path/",
},
{
name: "as subresource",
requestURL: "/namespaces/default/simple/id/subresource/another/different/path?param1=test1&param2=test2",
expectedPath: "another/different/path",
expectedPath: "/another/different/path",
},
{
name: "cluster-scoped basic",
@ -2155,13 +2165,13 @@ func TestGetWithOptions(t *testing.T) {
name: "cluster-scoped basic with path",
rootScoped: true,
requestURL: "/simple/id/a/cluster/path?param1=test1&param2=test2",
expectedPath: "a/cluster/path",
expectedPath: "/a/cluster/path",
},
{
name: "cluster-scoped basic as subresource",
rootScoped: true,
requestURL: "/simple/id/subresource/another/cluster/path?param1=test1&param2=test2",
expectedPath: "another/cluster/path",
expectedPath: "/another/cluster/path",
},
}
@ -2556,7 +2566,7 @@ func TestConnectWithOptions(t *testing.T) {
func TestConnectWithOptionsAndPath(t *testing.T) {
responseText := "Hello World"
itemID := "theID"
testPath := "a/b/c/def"
testPath := "/a/b/c/def"
connectStorage := &ConnecterRESTStorage{
connectHandler: &OutputConnect{
response: responseText,
@ -2572,7 +2582,7 @@ func TestConnectWithOptionsAndPath(t *testing.T) {
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect/" + testPath + "?param1=value1&param2=value2")
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect" + testPath + "?param1=value1&param2=value2")
if err != nil {
t.Errorf("unexpected error: %v", err)

View File

@ -212,7 +212,20 @@ func getRequestOptions(req *http.Request, scope RequestScope, into runtime.Objec
if isSubresource {
startingIndex = 3
}
newQuery[subpathKey] = []string{strings.Join(requestInfo.Parts[startingIndex:], "/")}
p := strings.Join(requestInfo.Parts[startingIndex:], "/")
// ensure non-empty subpaths correctly reflect a leading slash
if len(p) > 0 && !strings.HasPrefix(p, "/") {
p = "/" + p
}
// ensure subpaths correctly reflect the presence of a trailing slash on the original request
if strings.HasSuffix(requestInfo.Path, "/") && !strings.HasSuffix(p, "/") {
p += "/"
}
newQuery[subpathKey] = []string{p}
query = newQuery
}
return scope.ParameterCodec.DecodeParameters(query, scope.Kind.GroupVersion(), into)