mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-06 10:43:56 +00:00
Preserve leading and trailing slashes on proxy subpaths
This commit is contained in:
parent
158f6b78da
commit
04eede9b2a
@ -14,6 +14,7 @@ go_library(
|
|||||||
"//pkg/kubelet/client:go_default_library",
|
"//pkg/kubelet/client:go_default_library",
|
||||||
"//pkg/registry/core/node:go_default_library",
|
"//pkg/registry/core/node:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime: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/apimachinery/pkg/util/proxy:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
||||||
|
@ -20,9 +20,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/proxy"
|
"k8s.io/apimachinery/pkg/util/proxy"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ go_library(
|
|||||||
"//pkg/registry/core/pod:go_default_library",
|
"//pkg/registry/core/pod:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/errors: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/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/apimachinery/pkg/util/proxy:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
|
||||||
|
@ -20,9 +20,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/proxy"
|
"k8s.io/apimachinery/pkg/util/proxy"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
@ -71,7 +71,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, false, responder), nil
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/proxy"
|
"k8s.io/apimachinery/pkg/util/proxy"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
@ -66,7 +66,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -33,6 +34,26 @@ import (
|
|||||||
"golang.org/x/net/http2"
|
"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
|
// IsProbableEOF returns true if the given error resembles a connection termination
|
||||||
// scenario that would justify assuming that the watch is empty.
|
// 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
|
// These errors are what the Go http stack returns back to us which are general
|
||||||
|
@ -20,6 +20,7 @@ package net
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -218,3 +219,40 @@ func TestTLSClientConfigHolder(t *testing.T) {
|
|||||||
t.Errorf("didn't find tls config")
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2135,15 +2135,25 @@ func TestGetWithOptions(t *testing.T) {
|
|||||||
requestURL: "/namespaces/default/simple/id?param1=test1¶m2=test2",
|
requestURL: "/namespaces/default/simple/id?param1=test1¶m2=test2",
|
||||||
expectedPath: "",
|
expectedPath: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with root slash",
|
||||||
|
requestURL: "/namespaces/default/simple/id/?param1=test1¶m2=test2",
|
||||||
|
expectedPath: "/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "with path",
|
name: "with path",
|
||||||
requestURL: "/namespaces/default/simple/id/a/different/path?param1=test1¶m2=test2",
|
requestURL: "/namespaces/default/simple/id/a/different/path?param1=test1¶m2=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¶m2=test2",
|
||||||
|
expectedPath: "/a/different/path/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "as subresource",
|
name: "as subresource",
|
||||||
requestURL: "/namespaces/default/simple/id/subresource/another/different/path?param1=test1¶m2=test2",
|
requestURL: "/namespaces/default/simple/id/subresource/another/different/path?param1=test1¶m2=test2",
|
||||||
expectedPath: "another/different/path",
|
expectedPath: "/another/different/path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cluster-scoped basic",
|
name: "cluster-scoped basic",
|
||||||
@ -2155,13 +2165,13 @@ func TestGetWithOptions(t *testing.T) {
|
|||||||
name: "cluster-scoped basic with path",
|
name: "cluster-scoped basic with path",
|
||||||
rootScoped: true,
|
rootScoped: true,
|
||||||
requestURL: "/simple/id/a/cluster/path?param1=test1¶m2=test2",
|
requestURL: "/simple/id/a/cluster/path?param1=test1¶m2=test2",
|
||||||
expectedPath: "a/cluster/path",
|
expectedPath: "/a/cluster/path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cluster-scoped basic as subresource",
|
name: "cluster-scoped basic as subresource",
|
||||||
rootScoped: true,
|
rootScoped: true,
|
||||||
requestURL: "/simple/id/subresource/another/cluster/path?param1=test1¶m2=test2",
|
requestURL: "/simple/id/subresource/another/cluster/path?param1=test1¶m2=test2",
|
||||||
expectedPath: "another/cluster/path",
|
expectedPath: "/another/cluster/path",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2556,7 +2566,7 @@ func TestConnectWithOptions(t *testing.T) {
|
|||||||
func TestConnectWithOptionsAndPath(t *testing.T) {
|
func TestConnectWithOptionsAndPath(t *testing.T) {
|
||||||
responseText := "Hello World"
|
responseText := "Hello World"
|
||||||
itemID := "theID"
|
itemID := "theID"
|
||||||
testPath := "a/b/c/def"
|
testPath := "/a/b/c/def"
|
||||||
connectStorage := &ConnecterRESTStorage{
|
connectStorage := &ConnecterRESTStorage{
|
||||||
connectHandler: &OutputConnect{
|
connectHandler: &OutputConnect{
|
||||||
response: responseText,
|
response: responseText,
|
||||||
@ -2572,7 +2582,7 @@ func TestConnectWithOptionsAndPath(t *testing.T) {
|
|||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect/" + testPath + "?param1=value1¶m2=value2")
|
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect" + testPath + "?param1=value1¶m2=value2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
@ -212,7 +212,20 @@ func getRequestOptions(req *http.Request, scope RequestScope, into runtime.Objec
|
|||||||
if isSubresource {
|
if isSubresource {
|
||||||
startingIndex = 3
|
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
|
query = newQuery
|
||||||
}
|
}
|
||||||
return scope.ParameterCodec.DecodeParameters(query, scope.Kind.GroupVersion(), into)
|
return scope.ParameterCodec.DecodeParameters(query, scope.Kind.GroupVersion(), into)
|
||||||
|
Loading…
Reference in New Issue
Block a user