mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 21:52:02 +08:00
A new `api/client/proxy/Client` has been added to interact with the SSH and gRPC servers that the Proxy serves on its SSH port. The client will first try connecting to the gRPC server and if that fails it will fall back to the SSH server. Much of the SSH functionality mimics the existing behavior of the `ProxyClient` in `lib/client`. This is the first part of phasing out that client in favor of the new client. There will be a follow up PR that migrates `lib/client` to make use of the new client instead. Part of #19812
429 lines
13 KiB
Go
429 lines
13 KiB
Go
/*
|
|
Copyright 2017 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package utils
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/net/http/httpproxy"
|
|
)
|
|
|
|
func TestGetProxyAddress(t *testing.T) {
|
|
type env struct {
|
|
name string
|
|
val string
|
|
}
|
|
var tests = []struct {
|
|
info string
|
|
env []env
|
|
targetAddr string
|
|
proxyAddr string
|
|
}{
|
|
{
|
|
info: "valid, can be raw host:port",
|
|
env: []env{{name: "http_proxy", val: "proxy:1234"}},
|
|
proxyAddr: "proxy:1234",
|
|
targetAddr: "192.168.1.1:3030",
|
|
},
|
|
{
|
|
info: "valid, raw host:port works for https",
|
|
env: []env{{name: "HTTPS_PROXY", val: "proxy:1234"}},
|
|
proxyAddr: "proxy:1234",
|
|
targetAddr: "192.168.1.1:3030",
|
|
},
|
|
{
|
|
info: "valid, correct full url",
|
|
env: []env{{name: "https_proxy", val: "https://proxy:1234"}},
|
|
proxyAddr: "proxy:1234",
|
|
targetAddr: "192.168.1.1:3030",
|
|
},
|
|
{
|
|
info: "valid, http endpoint can be set in https_proxy",
|
|
env: []env{{name: "https_proxy", val: "http://proxy:1234"}},
|
|
proxyAddr: "proxy:1234",
|
|
targetAddr: "192.168.1.1:3030",
|
|
},
|
|
{
|
|
info: "valid, socks5 endpoint can be set in https_proxy",
|
|
env: []env{{name: "https_proxy", val: "socks5://proxy:1234"}},
|
|
proxyAddr: "proxy:1234",
|
|
targetAddr: "192.168.1.1:3030",
|
|
},
|
|
{
|
|
info: "valid, http endpoint can be set in https_proxy, but no_proxy override matches domain",
|
|
env: []env{
|
|
{name: "https_proxy", val: "http://proxy:1234"},
|
|
{name: "no_proxy", val: "proxy"}},
|
|
proxyAddr: "",
|
|
targetAddr: "proxy:1234",
|
|
},
|
|
{
|
|
info: "valid, http endpoint can be set in https_proxy, but no_proxy override matches ip",
|
|
env: []env{
|
|
{name: "https_proxy", val: "http://proxy:1234"},
|
|
{name: "no_proxy", val: "192.168.1.1"}},
|
|
proxyAddr: "",
|
|
targetAddr: "192.168.1.1:1234",
|
|
},
|
|
{
|
|
info: "valid, http endpoint can be set in https_proxy, but no_proxy override matches subdomain",
|
|
env: []env{
|
|
{name: "https_proxy", val: "http://proxy:1234"},
|
|
{name: "no_proxy", val: ".example.com"}},
|
|
proxyAddr: "",
|
|
targetAddr: "bla.example.com:1234",
|
|
},
|
|
{
|
|
info: "valid, no_proxy blocks matching port",
|
|
env: []env{
|
|
{name: "https_proxy", val: "proxy:9999"},
|
|
{name: "no_proxy", val: "example.com:1234"},
|
|
},
|
|
proxyAddr: "",
|
|
targetAddr: "example.com:1234",
|
|
},
|
|
{
|
|
info: "valid, no_proxy matches host but not port",
|
|
env: []env{
|
|
{name: "https_proxy", val: "proxy:9999"},
|
|
{name: "no_proxy", val: "example.com:1234"},
|
|
},
|
|
proxyAddr: "proxy:9999",
|
|
targetAddr: "example.com:5678",
|
|
},
|
|
}
|
|
|
|
// used to augment test cases with auth credentials
|
|
authTests := []struct {
|
|
info string
|
|
user string
|
|
password string
|
|
}{
|
|
{info: "no credentials", user: "", password: ""},
|
|
{info: "plain password", user: "alice", password: "password"},
|
|
{info: "special characters in password", user: "alice", password: " !@#$%^&*()_+-=[]{};:,.<>/?`~\"\\ abc123"},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
for j, authTest := range authTests {
|
|
t.Run(fmt.Sprintf("%v %v: %v with %v", i, j, tt.info, authTest.info), func(t *testing.T) {
|
|
for _, env := range tt.env {
|
|
switch strings.ToLower(env.name) {
|
|
case "http_proxy", "https_proxy":
|
|
// add auth test credentials into http(s)_proxy env vars
|
|
val, err := buildProxyAddr(env.val, authTest.user, authTest.password)
|
|
require.NoError(t, err)
|
|
t.Setenv(env.name, val)
|
|
case "no_proxy":
|
|
t.Setenv(env.name, env.val)
|
|
}
|
|
}
|
|
p := GetProxyURL(tt.targetAddr)
|
|
|
|
// is a proxy expected?
|
|
if tt.proxyAddr == "" {
|
|
require.Nil(t, p)
|
|
return
|
|
}
|
|
require.NotNil(t, p)
|
|
require.Equal(t, tt.proxyAddr, p.Host)
|
|
|
|
// are auth credentials expected?
|
|
if authTest.user == "" && authTest.password == "" {
|
|
require.Nil(t, p.User)
|
|
return
|
|
}
|
|
require.NotNil(t, p.User)
|
|
require.Equal(t, authTest.user, p.User.Username())
|
|
password, _ := p.User.Password()
|
|
require.Equal(t, authTest.password, password)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildProxyAddr(addr, user, pass string) (string, error) {
|
|
if user == "" && pass == "" {
|
|
return addr, nil
|
|
}
|
|
userInfo := url.UserPassword(user, pass)
|
|
if strings.HasPrefix(addr, "http") || strings.HasPrefix(addr, "socks5") {
|
|
u, err := url.Parse(addr)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
u.User = userInfo
|
|
return u.String(), nil
|
|
}
|
|
return fmt.Sprintf("%v@%v", userInfo.String(), addr), nil
|
|
}
|
|
|
|
func TestProxyAwareRoundTripper(t *testing.T) {
|
|
t.Setenv("HTTP_PROXY", "http://localhost:8888")
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
|
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
|
|
},
|
|
}
|
|
rt := NewHTTPRoundTripper(transport, nil)
|
|
req, err := http.NewRequest(http.MethodGet, "https://localhost:9999", nil)
|
|
require.NoError(t, err)
|
|
// Don't care about response, only if the scheme changed.
|
|
//nolint:bodyclose // resp should be nil, so there will be no body to close.
|
|
_, err = rt.RoundTrip(req)
|
|
require.Error(t, err)
|
|
require.Equal(t, "http", req.URL.Scheme)
|
|
}
|
|
|
|
// TestHttpRoundTripperDowngrade tests that the round tripper downgrades https requests to http
|
|
// when HTTP_PROXY is set to "http://localhost:*" (i.e. there's an http proxy running on localhost).
|
|
func TestHttpRoundTripperDowngrade(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
setHTTPProxy bool
|
|
shouldHitProxy bool
|
|
}{
|
|
{
|
|
desc: "hits http proxy if insecure and localhost http proxy is set",
|
|
setHTTPProxy: true,
|
|
shouldHitProxy: true,
|
|
},
|
|
{
|
|
desc: "does not hit http proxy if insecure and localhost http proxy is not set",
|
|
setHTTPProxy: false,
|
|
shouldHitProxy: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
newHandler := func(runningAtProxy bool, wasHit *bool) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
*wasHit = true
|
|
if tc.shouldHitProxy {
|
|
// If the request should hit the proxy, then:
|
|
// - this handler is running at the proxy, and
|
|
// - the scheme should be http.
|
|
require.True(t, runningAtProxy)
|
|
require.Equal(t, "http", r.URL.Scheme)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// Start localhost http proxy.
|
|
runningAtProxy := true
|
|
loopback := true
|
|
https := false
|
|
httpProxyWasHit := false
|
|
httpProxy, err := newServer(newHandler(runningAtProxy, &httpProxyWasHit), loopback, https)
|
|
require.NoError(t, err)
|
|
defer httpProxy.Close()
|
|
|
|
// Start non-localhost https server.
|
|
runningAtProxy = false
|
|
loopback = false
|
|
https = true
|
|
httpsSrvWasHit := false
|
|
httpsSrv, err := newServer(newHandler(runningAtProxy, &httpsSrvWasHit), loopback, https)
|
|
require.NoError(t, err)
|
|
defer httpsSrv.Close()
|
|
|
|
if tc.setHTTPProxy {
|
|
// url.Parse won't correctly parse an absolute URL without a scheme.
|
|
u, err := url.Parse("http://" + httpProxy.Listener.Addr().String())
|
|
require.NoError(t, err)
|
|
_, port, err := net.SplitHostPort(u.Host)
|
|
require.NoError(t, err)
|
|
|
|
// Set HTTP_PROXY to "http://localhost:*".
|
|
t.Setenv("HTTP_PROXY", fmt.Sprintf("http://localhost:%s", port))
|
|
}
|
|
|
|
clt := newClient(t, nil)
|
|
|
|
// Perform any request.
|
|
// Set addr to the https server. If HTTP_PROXY was set above,
|
|
// the http proxy should be hit regardless.
|
|
addr := httpsSrv.Listener.Addr().String()
|
|
request(t, clt, addr)
|
|
|
|
// Validate that the correct server was hit.
|
|
require.Equal(t, tc.shouldHitProxy, httpProxyWasHit)
|
|
require.Equal(t, !tc.shouldHitProxy, httpsSrvWasHit)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHttpRoundTripperExtraHeaders tests that the round tripper adds the extra headers set.
|
|
func TestHttpRoundTripperExtraHeaders(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
extraHeaders map[string]string
|
|
expectHeaders func(*testing.T, http.Header)
|
|
}{
|
|
{
|
|
desc: "extra headers are added",
|
|
extraHeaders: map[string]string{
|
|
"header1": "value1",
|
|
"header2": "value2",
|
|
},
|
|
expectHeaders: func(t *testing.T, headers http.Header) {
|
|
require.Equal(t, []string{"value1"}, headers.Values("header1"))
|
|
require.Equal(t, []string{"value2"}, headers.Values("header2"))
|
|
},
|
|
},
|
|
{
|
|
desc: "extra headers do not overwrite existing headers",
|
|
extraHeaders: map[string]string{
|
|
"header1": "value1",
|
|
"Content-Type": "value2",
|
|
},
|
|
expectHeaders: func(t *testing.T, headers http.Header) {
|
|
require.Equal(t, []string{"value1"}, headers.Values("header1"))
|
|
require.Equal(t, []string{"application/json", "value2"}, headers.Values("Content-Type"))
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
tc.expectHeaders(t, r.Header)
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// Start localhost https server.
|
|
loopback := true
|
|
tls := true
|
|
httpsSrv, err := newServer(handler, loopback, tls)
|
|
require.NoError(t, err)
|
|
defer httpsSrv.Close()
|
|
|
|
clt := newClient(t, tc.extraHeaders)
|
|
|
|
// Perform any request.
|
|
// Set the address to the localhost https server.
|
|
addr := httpsSrv.Listener.Addr().String()
|
|
request(t, clt, addr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// newServer starts a new server that:
|
|
// - runs TLS if `https`
|
|
// - uses a loopback listener if `loopback`
|
|
func newServer(handler http.HandlerFunc, loopback bool, https bool) (*httptest.Server, error) {
|
|
srv := httptest.NewUnstartedServer(handler)
|
|
|
|
if !loopback {
|
|
// Replace the test-supplied loopback listener with the first available
|
|
// non-loopback address.
|
|
srv.Listener.Close()
|
|
l, err := net.Listen("tcp", "0.0.0.0:0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
srv.Listener = l
|
|
}
|
|
|
|
if https {
|
|
srv.StartTLS()
|
|
} else {
|
|
srv.Start()
|
|
}
|
|
return srv, nil
|
|
}
|
|
|
|
// newClient creates a new https roundtrip client.
|
|
func newClient(t *testing.T, extraHeaders map[string]string) *http.Client {
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
// Setting insecure ensures that https requests succeed.
|
|
InsecureSkipVerify: true,
|
|
},
|
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
|
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
|
|
},
|
|
}
|
|
return &http.Client{
|
|
Transport: NewHTTPRoundTripper(transport, extraHeaders),
|
|
}
|
|
}
|
|
|
|
// request perform a POST request.
|
|
func request(t *testing.T, clt *http.Client, addr string) {
|
|
url := "https://" + addr + "/v1/content"
|
|
resp, err := clt.Post(url, "application/json", nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
successTests := []struct {
|
|
name, addr, scheme, host, path string
|
|
}{
|
|
{name: "scheme-host-port", addr: "http://example.com:8080", scheme: "http", host: "example.com:8080", path: ""},
|
|
{name: "host-port", addr: "example.com:8080", scheme: "", host: "example.com:8080", path: ""},
|
|
{name: "scheme-ip4-port", addr: "http://127.0.0.1:8080", scheme: "http", host: "127.0.0.1:8080", path: ""},
|
|
{name: "ip4-port", addr: "127.0.0.1:8080", scheme: "", host: "127.0.0.1:8080", path: ""},
|
|
{name: "scheme-ip6-port", addr: "http://[::1]:8080", scheme: "http", host: "[::1]:8080", path: ""},
|
|
{name: "ip6-port", addr: "[::1]:8080", scheme: "", host: "[::1]:8080"},
|
|
{name: "host/path", addr: "example.com/path/to/somewhere", scheme: "", host: "example.com", path: "/path/to/somewhere"},
|
|
}
|
|
for _, tc := range successTests {
|
|
t.Run(fmt.Sprintf("should parse: %s", tc.name), func(t *testing.T) {
|
|
u, err := parse(tc.addr)
|
|
require.NoError(t, err)
|
|
errMsg := fmt.Sprintf("(%v, %v, %v)", u.Scheme, u.Host, u.Path)
|
|
require.Equal(t, tc.scheme, u.Scheme, errMsg)
|
|
require.Equal(t, tc.host, u.Host, errMsg)
|
|
require.Equal(t, tc.path, u.Path)
|
|
})
|
|
}
|
|
|
|
failTests := []struct {
|
|
name, addr string
|
|
}{
|
|
{name: "invalid char in host without scheme", addr: "bad addr"},
|
|
}
|
|
for _, tc := range failTests {
|
|
t.Run(fmt.Sprintf("should not parse: %s", tc.name), func(t *testing.T) {
|
|
u, err := parse(tc.addr)
|
|
require.Error(t, err, u)
|
|
})
|
|
}
|
|
|
|
t.Run("empty addr", func(t *testing.T) {
|
|
u, err := parse("")
|
|
require.NoError(t, err)
|
|
require.Nil(t, u)
|
|
})
|
|
}
|