Files
teleport/api/utils/proxy_test.go
T
Andrew Burke d29a6ed6c8 Add callback flag to tsh login (#35457)
This change adds the --callback flag to tsh login, which overrides
the base URL printed when doing an SSO login.
2024-01-09 19:32:58 +00:00

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 := ParseURL(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 := ParseURL(tc.addr)
require.Error(t, err, u)
})
}
t.Run("empty addr", func(t *testing.T) {
u, err := ParseURL("")
require.NoError(t, err)
require.Nil(t, u)
})
}