package validation import ( "bytes" "fmt" "io/ioutil" "testing" "time" "context" "crypto/tls" "crypto/x509" "net" "net/http" "net/http/httptest" "net/url" "strings" "github.com/stretchr/testify/assert" ) func TestValidateHostname(t *testing.T) { var inputHostname string hostname, err := ValidateHostname(inputHostname) assert.Equal(t, err, nil) assert.Empty(t, hostname) inputHostname = "hello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) inputHostname = "http://hello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) inputHostname = "bücher.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "xn--bcher-kva.example.com", hostname) inputHostname = "http://bücher.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "xn--bcher-kva.example.com", hostname) inputHostname = "http%3A%2F%2Fhello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) } func TestValidateUrl(t *testing.T) { validUrl, err := ValidateUrl("") assert.Equal(t, fmt.Errorf("URL should not be empty"), err) assert.Empty(t, validUrl) validUrl, err = ValidateUrl("https://localhost:8080") assert.Nil(t, err) assert.Equal(t, "https://localhost:8080", validUrl) validUrl, err = ValidateUrl("localhost:8080") assert.Nil(t, err) assert.Equal(t, "http://localhost:8080", validUrl) validUrl, err = ValidateUrl("http://localhost") assert.Nil(t, err) assert.Equal(t, "http://localhost", validUrl) validUrl, err = ValidateUrl("http://127.0.0.1:8080") assert.Nil(t, err) assert.Equal(t, "http://127.0.0.1:8080", validUrl) validUrl, err = ValidateUrl("127.0.0.1:8080") assert.Nil(t, err) assert.Equal(t, "http://127.0.0.1:8080", validUrl) validUrl, err = ValidateUrl("127.0.0.1") assert.Nil(t, err) assert.Equal(t, "http://127.0.0.1", validUrl) validUrl, err = ValidateUrl("https://127.0.0.1:8080") assert.Nil(t, err) assert.Equal(t, "https://127.0.0.1:8080", validUrl) validUrl, err = ValidateUrl("[::1]:8080") assert.Nil(t, err) assert.Equal(t, "http://[::1]:8080", validUrl) validUrl, err = ValidateUrl("http://[::1]") assert.Nil(t, err) assert.Equal(t, "http://[::1]", validUrl) validUrl, err = ValidateUrl("http://[::1]:8080") assert.Nil(t, err) assert.Equal(t, "http://[::1]:8080", validUrl) validUrl, err = ValidateUrl("[::1]") assert.Nil(t, err) assert.Equal(t, "http://[::1]", validUrl) validUrl, err = ValidateUrl("https://example.com") assert.Nil(t, err) assert.Equal(t, "https://example.com", validUrl) validUrl, err = ValidateUrl("example.com") assert.Nil(t, err) assert.Equal(t, "http://example.com", validUrl) validUrl, err = ValidateUrl("http://hello.example.com") assert.Nil(t, err) assert.Equal(t, "http://hello.example.com", validUrl) validUrl, err = ValidateUrl("hello.example.com") assert.Nil(t, err) assert.Equal(t, "http://hello.example.com", validUrl) validUrl, err = ValidateUrl("hello.example.com:8080") assert.Nil(t, err) assert.Equal(t, "http://hello.example.com:8080", validUrl) validUrl, err = ValidateUrl("https://hello.example.com:8080") assert.Nil(t, err) assert.Equal(t, "https://hello.example.com:8080", validUrl) validUrl, err = ValidateUrl("https://bücher.example.com") assert.Nil(t, err) assert.Equal(t, "https://xn--bcher-kva.example.com", validUrl) validUrl, err = ValidateUrl("bücher.example.com") assert.Nil(t, err) assert.Equal(t, "http://xn--bcher-kva.example.com", validUrl) validUrl, err = ValidateUrl("https%3A%2F%2Fhello.example.com") assert.Nil(t, err) assert.Equal(t, "https://hello.example.com", validUrl) validUrl, err = ValidateUrl("ftp://alex:12345@hello.example.com:8080/robot.txt") assert.Equal(t, "Currently Argo Tunnel does not support ftp protocol.", err.Error()) assert.Empty(t, validUrl) validUrl, err = ValidateUrl("https://alex:12345@hello.example.com:8080") assert.Nil(t, err) assert.Equal(t, "https://hello.example.com:8080", validUrl) } func TestToggleProtocol(t *testing.T) { assert.Equal(t, "https", toggleProtocol("http")) assert.Equal(t, "http", toggleProtocol("https")) assert.Equal(t, "random", toggleProtocol("random")) assert.Equal(t, "", toggleProtocol("")) } // Happy path 1: originURL is HTTP, and HTTP connections work func TestValidateHTTPService_HTTP2HTTP(t *testing.T) { originURL := "http://127.0.0.1/" hostname := "example.com" assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return emptyResponse(200), nil } if req.URL.Scheme == "https" { t.Fatal("http works, shouldn't have tried with https") } panic("Shouldn't reach here") }))) assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return emptyResponse(503), nil } if req.URL.Scheme == "https" { t.Fatal("http works, shouldn't have tried with https") } panic("Shouldn't reach here") }))) // Integration-style test with a mock server server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, hostname, r.Host) w.WriteHeader(200) })) assert.NoError(t, err) defer server.Close() assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport)) // this will fail if the client follows the 302 redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/followedRedirect" { t.Fatal("shouldn't have followed the 302") } if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:443", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.Header().Set("Location", "/followedRedirect") w.WriteHeader(302) })) assert.NoError(t, err) defer redirectServer.Close() assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport)) } // Happy path 2: originURL is HTTPS, and HTTPS connections work func TestValidateHTTPService_HTTPS2HTTPS(t *testing.T) { originURL := "https://127.0.0.1/" hostname := "example.com" assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { t.Fatal("https works, shouldn't have tried with http") } if req.URL.Scheme == "https" { return emptyResponse(200), nil } panic("Shouldn't reach here") }))) assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { t.Fatal("https works, shouldn't have tried with http") } if req.URL.Scheme == "https" { return emptyResponse(503), nil } panic("Shouldn't reach here") }))) // Integration-style test with a mock server server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:443", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.WriteHeader(200) })) assert.NoError(t, err) defer server.Close() assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport)) // this will fail if the client follows the 302 redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/followedRedirect" { t.Fatal("shouldn't have followed the 302") } if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:443", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.Header().Set("Location", "/followedRedirect") w.WriteHeader(302) })) assert.NoError(t, err) defer redirectServer.Close() assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport)) } // Error path 1: originURL is HTTPS, but HTTP connections work func TestValidateHTTPService_HTTPS2HTTP(t *testing.T) { originURL := "https://127.0.0.1:1234/" hostname := "example.com" assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return emptyResponse(200), nil } if req.URL.Scheme == "https" { return nil, assert.AnError } panic("Shouldn't reach here") }))) assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return emptyResponse(503), nil } if req.URL.Scheme == "https" { return nil, assert.AnError } panic("Shouldn't reach here") }))) // Integration-style test with a mock server server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:1234", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.WriteHeader(200) })) assert.NoError(t, err) defer server.Close() assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport)) // this will fail if the client follows the 302 redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/followedRedirect" { t.Fatal("shouldn't have followed the 302") } if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:1234", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.Header().Set("Location", "/followedRedirect") w.WriteHeader(302) })) assert.NoError(t, err) defer redirectServer.Close() assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport)) } // Error path 2: originURL is HTTP, but HTTPS connections work func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) { originURL := "http://127.0.0.1:1234/" hostname := "example.com" assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return nil, assert.AnError } if req.URL.Scheme == "https" { return emptyResponse(200), nil } panic("Shouldn't reach here") }))) assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.Host, hostname) if req.URL.Scheme == "http" { return nil, assert.AnError } if req.URL.Scheme == "https" { return emptyResponse(503), nil } panic("Shouldn't reach here") }))) // Integration-style test with a mock server server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:1234", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.WriteHeader(200) })) assert.NoError(t, err) defer server.Close() assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport)) // this will fail if the client follows the 302 redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/followedRedirect" { t.Fatal("shouldn't have followed the 302") } if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:443", r.Host) } else { assert.Equal(t, hostname, r.Host) } w.Header().Set("Location", "/followedRedirect") w.WriteHeader(302) })) assert.NoError(t, err) defer redirectServer.Close() assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport)) } // error path 3: origin URL is nonresponsive func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) { originURL := "https://127.0.0.1/" hostname := "example.com" oldValidationTimeout := validationTimeout defer func() { validationTimeout = oldValidationTimeout }() validationTimeout = 500 * time.Millisecond server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "CONNECT" { assert.Equal(t, "127.0.0.1:443", r.Host) } else { assert.Equal(t, hostname, r.Host) } time.Sleep(1 * time.Second) w.WriteHeader(200) })) if !assert.NoError(t, err) { t.FailNow() } defer server.Close() err = ValidateHTTPService(originURL, hostname, client.Transport) if err, ok := err.(net.Error); assert.True(t, ok) { assert.True(t, err.Timeout()) } } type testRoundTripper func(req *http.Request) (*http.Response, error) func (f testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func emptyResponse(statusCode int) *http.Response { return &http.Response{ StatusCode: statusCode, Body: ioutil.NopCloser(bytes.NewReader(nil)), Header: make(http.Header), } } func createMockServerAndClient(handler http.Handler) (*httptest.Server, *http.Client, error) { client := http.DefaultClient server := httptest.NewServer(handler) client.Transport = &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(server.URL) }, } return server, client, nil } func createSecureMockServerAndClient(handler http.Handler) (*httptest.Server, *http.Client, error) { client := http.DefaultClient server := httptest.NewTLSServer(handler) cert, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) if err != nil { server.Close() return nil, nil, err } certpool := x509.NewCertPool() certpool.AddCert(cert) client.Transport = &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return net.Dial("tcp", server.URL[strings.LastIndex(server.URL, "/")+1:]) }, TLSClientConfig: &tls.Config{ RootCAs: certpool, }, } return server, client, nil }