mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 14:35:22 +08:00
This change adds the --callback flag to tsh login, which overrides the base URL printed when doing an SSO login.
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 := 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)
|
|
})
|
|
}
|