diff options
author | Jacob Vosmaer <jacob@gitlab.com> | 2019-01-14 16:24:31 +0300 |
---|---|---|
committer | Jacob Vosmaer <jacob@gitlab.com> | 2019-01-14 16:24:31 +0300 |
commit | 98719c6d753b136caf169f5c10865c9c3497d88b (patch) | |
tree | 604f0aa593f8a7f75db7ec9a22e55fabea02dde9 /client | |
parent | b431ed8dc63f8bb1b449f358aaa56a21e8073058 (diff) | |
parent | 47da6b6b62b5f3f25ea4a2a1a52df03d1e879af4 (diff) |
Merge branch '1447-using-https_proxy-in-gitaly-environment-breaks-communication-with-rails-since-11-6' into 'master'
Fix regression for https_proxy and unix socket connections
Closes #1447
See merge request gitlab-org/gitaly!1032
Diffstat (limited to 'client')
-rw-r--r-- | client/address_parser.go | 32 | ||||
-rw-r--r-- | client/address_parser_test.go | 74 | ||||
-rw-r--r-- | client/dial.go | 70 | ||||
-rw-r--r-- | client/dial_test.go | 250 | ||||
-rw-r--r-- | client/testdata/gitalycert.pem | 30 | ||||
-rw-r--r-- | client/testdata/gitalykey.pem | 52 |
6 files changed, 465 insertions, 43 deletions
diff --git a/client/address_parser.go b/client/address_parser.go index 55969e909..a052342ae 100644 --- a/client/address_parser.go +++ b/client/address_parser.go @@ -3,22 +3,36 @@ package client import ( "fmt" "net/url" + "strings" ) -func parseAddress(rawAddress string) (canonicalAddress string, err error) { +// extractHostFromRemoteURL will convert Gitaly-style URL addresses of the form +// scheme://host:port to the "host:port" addresses used by `grpc.Dial` +func extractHostFromRemoteURL(rawAddress string) (hostAndPort string, err error) { u, err := url.Parse(rawAddress) if err != nil { return "", err } - // tcp:// addresses are a special case which `grpc.Dial` expects in a - // different format - if u.Scheme == "tcp" || u.Scheme == "tls" { - if u.Path != "" { - return "", fmt.Errorf("%s addresses should not have a path", u.Scheme) - } - return u.Host, nil + if u.Path != "" { + return "", fmt.Errorf("remote addresses should not have a path") } - return u.String(), nil + if u.Host == "" { + return "", fmt.Errorf("remote addresses should have a host") + } + + return u.Host, nil +} + +// extractPathFromSocketURL will convert Gitaly-style URL addresses of the form +// unix:/path/to/socket into file paths: `/path/to/socket` +const unixPrefix = "unix:" + +func extractPathFromSocketURL(rawAddress string) (socketPath string, err error) { + if !strings.HasPrefix(rawAddress, unixPrefix) { + return "", fmt.Errorf("invalid socket address: %s", rawAddress) + } + + return strings.TrimPrefix(rawAddress, unixPrefix), nil } diff --git a/client/address_parser_test.go b/client/address_parser_test.go index 820a902b3..f5dc2f31b 100644 --- a/client/address_parser_test.go +++ b/client/address_parser_test.go @@ -2,19 +2,16 @@ package client import ( "testing" + + "github.com/stretchr/testify/require" ) -func TestParseAddress(t *testing.T) { +func Test_extractHostFromRemoteURL(t *testing.T) { testCases := []struct { raw string canonical string invalid bool }{ - {raw: "unix:/foo/bar.socket", canonical: "unix:///foo/bar.socket"}, - {raw: "unix:///foo/bar.socket", canonical: "unix:///foo/bar.socket"}, - // Mainly for test purposes we explicitly want to support relative paths - {raw: "unix://foo/bar.socket", canonical: "unix://foo/bar.socket"}, - {raw: "unix:foo/bar.socket", canonical: "unix:foo/bar.socket"}, {raw: "tcp://1.2.3.4", canonical: "1.2.3.4"}, {raw: "tcp://1.2.3.4:567", canonical: "1.2.3.4:567"}, {raw: "tcp://foobar", canonical: "foobar"}, @@ -24,28 +21,53 @@ func TestParseAddress(t *testing.T) { {raw: "tcp:///foo/bar.socket", invalid: true}, {raw: "tcp:/foo/bar.socket", invalid: true}, {raw: "tcp://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9999", canonical: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9999"}, - {raw: "foobar:9999", canonical: "foobar:9999"}, - // As per https://github.com/grpc/grpc/blob/master/doc/naming.md... - {raw: "dns:///127.0.0.1:9999", canonical: "dns:///127.0.0.1:9999"}, - {raw: "dns:///[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9999", canonical: "dns:///[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9999"}, + {raw: "foobar:9999", invalid: true}, + {raw: "unix:/foo/bar.socket", invalid: true}, + {raw: "unix:///foo/bar.socket", invalid: true}, + {raw: "unix://foo/bar.socket", invalid: true}, + {raw: "unix:foo/bar.socket", invalid: true}, + } + + for _, tc := range testCases { + t.Run(tc.raw, func(t *testing.T) { + canonical, err := extractHostFromRemoteURL(tc.raw) + if tc.invalid { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.canonical, canonical) + }) + } +} + +func Test_extractPathFromSocketURL(t *testing.T) { + testCases := []struct { + raw string + path string + invalid bool + }{ + {raw: "unix:/foo/bar.socket", path: "/foo/bar.socket"}, + {raw: "unix:///foo/bar.socket", path: "///foo/bar.socket"}, // Silly but valid + {raw: "unix:foo/bar.socket", path: "foo/bar.socket"}, + {raw: "unix:../foo/bar.socket", path: "../foo/bar.socket"}, + {raw: "unix:path/with/a/colon:/in/it", path: "path/with/a/colon:/in/it"}, + {raw: "tcp://1.2.3.4", invalid: true}, + {raw: "foo/bar.socket", invalid: true}, } for _, tc := range testCases { - canonical, err := parseAddress(tc.raw) - - if err == nil && tc.invalid { - t.Errorf("%v: expected error, got none", tc) - } else if err != nil && !tc.invalid { - t.Errorf("%v: parse error: %v", tc, err) - continue - } - - if tc.invalid { - continue - } - - if tc.canonical != canonical { - t.Errorf("%v: expected %q, got %q", tc, tc.canonical, canonical) - } + t.Run(tc.raw, func(t *testing.T) { + path, err := extractPathFromSocketURL(tc.raw) + + if tc.invalid { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.path, path) + }) } } diff --git a/client/dial.go b/client/dial.go index d0a51c0c1..fe4a3e683 100644 --- a/client/dial.go +++ b/client/dial.go @@ -1,6 +1,10 @@ package client import ( + "fmt" + "net" + "time" + "google.golang.org/grpc/credentials" "net/url" @@ -11,14 +15,30 @@ import ( // DefaultDialOpts hold the default DialOptions for connection to Gitaly over UNIX-socket var DefaultDialOpts = []grpc.DialOption{} +type connectionType int + +const ( + invalidConnection connectionType = iota + tcpConnection + tlsConnection + unixConnection +) + // Dial gitaly func Dial(rawAddress string, connOpts []grpc.DialOption) (*grpc.ClientConn, error) { - canonicalAddress, err := parseAddress(rawAddress) - if err != nil { - return nil, err - } + var canonicalAddress string + var err error + + switch getConnectionType(rawAddress) { + case invalidConnection: + return nil, fmt.Errorf("invalid connection string: %s", rawAddress) + + case tlsConnection: + canonicalAddress, err = extractHostFromRemoteURL(rawAddress) // Ensure the form: "host:port" ... + if err != nil { + return nil, err + } - if isTLS(rawAddress) { certPool, err := systemCertPool() if err != nil { return nil, err @@ -26,8 +46,29 @@ func Dial(rawAddress string, connOpts []grpc.DialOption) (*grpc.ClientConn, erro creds := credentials.NewClientTLSFromCert(certPool, "") connOpts = append(connOpts, grpc.WithTransportCredentials(creds)) - } else { + + case tcpConnection: + canonicalAddress, err = extractHostFromRemoteURL(rawAddress) // Ensure the form: "host:port" ... + if err != nil { + return nil, err + } connOpts = append(connOpts, grpc.WithInsecure()) + + case unixConnection: + canonicalAddress = rawAddress // This will be overriden by the custom dialer... + connOpts = append( + connOpts, + grpc.WithInsecure(), + grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + path, err := extractPathFromSocketURL(addr) + if err != nil { + return nil, err + } + + return net.DialTimeout("unix", path, timeout) + }), + ) + } conn, err := grpc.Dial(canonicalAddress, connOpts...) @@ -38,7 +79,20 @@ func Dial(rawAddress string, connOpts []grpc.DialOption) (*grpc.ClientConn, erro return conn, nil } -func isTLS(rawAddress string) bool { +func getConnectionType(rawAddress string) connectionType { u, err := url.Parse(rawAddress) - return err == nil && u.Scheme == "tls" + if err != nil { + return invalidConnection + } + + switch u.Scheme { + case "tls": + return tlsConnection + case "unix": + return unixConnection + case "tcp": + return tcpConnection + default: + return invalidConnection + } } diff --git a/client/dial_test.go b/client/dial_test.go new file mode 100644 index 000000000..a7edd5598 --- /dev/null +++ b/client/dial_test.go @@ -0,0 +1,250 @@ +package client + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" +) + +var proxyEnvironmentKeys = []string{"http_proxy", "https_proxy", "no_proxy"} + +func doDialAndExecuteCall(addr string) error { + conn, err := Dial(addr, nil) + if err != nil { + return fmt.Errorf("dial: %v", err) + } + + client := healthpb.NewHealthClient(conn) + _, err = client.Check(context.Background(), &healthpb.HealthCheckRequest{}) + return err +} + +func TestDial(t *testing.T) { + if emitProxyWarning() { + t.Log("WARNING. Proxy configuration detected from environment settings. This test failure may be related to proxy configuration. Please process with caution") + } + + stop, connectionMap, err := startListeners() + require.NoError(t, err, "start listeners: %v. %s", err) + defer stop() + + wd, err := os.Getwd() + require.NoError(t, err, "getwd: %v. %s", err) + + unixSocketAbsPath := connectionMap["unix"] + unixSocketRelPath, err := filepath.Rel(wd, unixSocketAbsPath) + require.NoError(t, err, "relative path failure: %v. %s", err) + + tests := []struct { + name string + rawAddress string + envSSLCertFile string + expectFailure bool + }{ + { + name: "tcp localhost with prefix", + rawAddress: "tcp://localhost:" + connectionMap["tcp"], // "tcp://localhost:1234" + expectFailure: false, + }, + { + name: "tls localhost", + rawAddress: "tls://localhost:" + connectionMap["tls"], // "tls://localhost:1234" + envSSLCertFile: "./testdata/gitalycert.pem", + expectFailure: false, + }, + { + name: "unix absolute", + rawAddress: "unix:" + unixSocketAbsPath, // "unix:/tmp/temp-socket" + expectFailure: false, + }, + { + name: "unix relative", + rawAddress: "unix:" + unixSocketRelPath, // "unix:../../tmp/temp-socket" + expectFailure: false, + }, + { + name: "unix absolute does not exist", + rawAddress: "unix:" + unixSocketAbsPath + ".does_not_exist", // "unix:/tmp/temp-socket.does_not_exist" + expectFailure: true, + }, + { + name: "unix relative does not exist", + rawAddress: "unix:" + unixSocketRelPath + ".does_not_exist", // "unix:../../tmp/temp-socket.does_not_exist" + expectFailure: true, + }, + { + // Gitaly does not support connections that do not have a scheme. + name: "tcp localhost no prefix", + rawAddress: "localhost:" + connectionMap["tcp"], // "localhost:1234" + expectFailure: true, + }, + { + name: "invalid", + rawAddress: ".", + expectFailure: true, + }, + { + name: "empty", + rawAddress: "", + expectFailure: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if emitProxyWarning() { + t.Log("WARNING. Proxy configuration detected from environment settings. This test failure may be related to proxy configuration. Please process with caution") + } + + if tt.envSSLCertFile != "" { + restore := modifyEnvironment("SSL_CERT_FILE", tt.envSSLCertFile) + defer restore() + } + + err := doDialAndExecuteCall(tt.rawAddress) + if tt.expectFailure { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +// healthServer provide a basic GRPC health service endpoint for testing purposes +type healthServer struct { +} + +func (*healthServer) Check(context.Context, *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil +} + +func (*healthServer) Watch(*healthpb.HealthCheckRequest, healthpb.Health_WatchServer) error { + return status.Errorf(codes.Unimplemented, "Not implemented") +} + +// startTCPListener will start a insecure TCP listener on a random unused port +func startTCPListener() (func(), string, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return nil, "", err + } + tcpPort := listener.Addr().(*net.TCPAddr).Port + address := fmt.Sprintf("%d", tcpPort) + + grpcServer := grpc.NewServer() + healthpb.RegisterHealthServer(grpcServer, &healthServer{}) + go grpcServer.Serve(listener) + + return func() { + grpcServer.Stop() + }, address, nil +} + +// startUnixListener will start a unix socket listener using a temporary file +func startUnixListener() (func(), string, error) { + serverSocketPath := testhelper.GetTemporaryGitalySocketFileName() + + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + return nil, "", err + } + + grpcServer := grpc.NewServer() + healthpb.RegisterHealthServer(grpcServer, &healthServer{}) + go grpcServer.Serve(listener) + + return func() { + grpcServer.Stop() + }, serverSocketPath, nil +} + +// startTLSListener will start a secure TLS listener on a random unused port +func startTLSListener() (func(), string, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return nil, "", err + } + tcpPort := listener.Addr().(*net.TCPAddr).Port + address := fmt.Sprintf("%d", tcpPort) + + cert, err := tls.LoadX509KeyPair("./testdata/gitalycert.pem", "./testdata/gitalykey.pem") + if err != nil { + return nil, "", err + } + + grpcServer := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))) + healthpb.RegisterHealthServer(grpcServer, &healthServer{}) + go grpcServer.Serve(listener) + + return func() { + grpcServer.Stop() + }, address, nil +} + +var listeners = map[string]func() (func(), string, error){ + "tcp": startTCPListener, + "unix": startUnixListener, + "tls": startTLSListener, +} + +// startListeners will start all the different listeners used in this test +func startListeners() (func(), map[string]string, error) { + var closers []func() + connectionMap := map[string]string{} + for k, v := range listeners { + closer, address, err := v() + if err != nil { + return nil, nil, err + } + closers = append(closers, closer) + connectionMap[k] = address + } + + return func() { + for _, v := range closers { + v() + } + }, connectionMap, nil +} + +// modifyEnvironment will change an environment variable and return a func suitable +// for `defer` to change the value back. +func modifyEnvironment(key string, value string) func() { + oldValue, hasOldValue := os.LookupEnv(key) + os.Setenv(key, value) + return func() { + if hasOldValue { + os.Setenv(key, oldValue) + } else { + os.Unsetenv(key) + } + } +} + +func emitProxyWarning() bool { + for _, key := range proxyEnvironmentKeys { + value := os.Getenv(key) + if value != "" { + return true + } + value = os.Getenv(strings.ToUpper(key)) + if value != "" { + return true + } + } + return false +} diff --git a/client/testdata/gitalycert.pem b/client/testdata/gitalycert.pem new file mode 100644 index 000000000..8b1514548 --- /dev/null +++ b/client/testdata/gitalycert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFODCCAyACCQDpPfNtveVc8TANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMQ8wDQYDVQQKDAZHaXRMYWIxDzAN +BgNVBAsMBmdpdGFseTESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MTEwMjA5MDIx +MloYDzIxMTgxMDA5MDkwMjEyWjBdMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVVMx +CzAJBgNVBAcMAlVTMQ8wDQYDVQQKDAZHaXRMYWIxDzANBgNVBAsMBmdpdGFseTES +MBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEApJXJOWpUkV32v8gRXLWn6TEsQmy2WeilQXg96V6VOQjGZAGMEJLEjH9WHBNe +Zi4V+W+j1FB8vWTNRGTcOcpSEmDFuewoBJVA8dFtNF4jj7QQymmnKeDuOW4fWLeU +YcyGxyjlpkm2+DUg5CavT4bMZILqbsAavxJ8SKCdJpMtW3sxklnGuTHcAckHldab +9ZxH/qYqLxc5Ek2BK4OibBxA84h1RUsqe2EdzZUOoet3xpwG3Vr8bGPqR7Psghs6 +TDdWU8hYYHlReCWezgZHiYDoRqY9HCZrHSpUZ1lbRo++2j4bvdFHOAUm4BEQ6fFc +sgtW+xkNK8bxj9XTcpuDrEVscv3fyBlCMSvD+HpNbr2k1oZSOFhxISIwBLKWQBjq +5muvMRbmrG5RgWqMWjXb+g0UmlyMa2YWAWsBgSuUSjJePgbUZWHuxp/dM8CQ4lHJ +ADvfSI9ysJQM/trqjRu5BRhxiKWR72QSi1qpDPT0nKWlzQ58zs3RSuOJbWm8oOqr +XL9G/XmvgzK1qwToI/WmXBeaqmfpkagYZm+TJW0GVnDqTC+EoXdFKW7aWIjlcb4p +tYoiRA/2jjq5OqeV6iKnxz7mEJQR1xDebm6+AWgFy4zyB/QvzanaUTvNiLhyBy6Q +YwXJHkNh+KrVszBlXxkARrGesXgqOznmDeErkOKDjxzQv+cCAwEAATANBgkqhkiG +9w0BAQsFAAOCAgEAk83b9wY9iwRrx5Yep3DA3xZkVu3GJcKf0tTL8apP1MzVBSUK +5tkvW2Z4D41jpZWgJDRF8/nT2lvVwvd5xQ8/oTUerFeG/ZZ+AiBagkBKl8piPHqD +cefAO8N2SKoYHV4xBeoVU6InUuJ7xu7BLF6tY3xKvx0XsjGC7B621xmq+E56dPZg +sQwekkxODbUw4NekqYFY21BT4xiWVrTRLIGY9AfV9Ry4gqQTxda7yst4ykWh1a9e +O+426uz3jshzpQTjZwk8kCZquJKa8Qzqfdlevns0FQDP5jck4BH/YkMNsa/g9XCd +ZHSB7gqAfNoNTB1rqNKIfPUF4mTu/RWMVwxb8f6h0TfywHZ4q/4R3Zfu3jUyeVVY +ziJu2CJpcoR9SESKFbN4WFzk91nIhf2pCGo/qNO5f+n5ZPnS2jrrWL5h64e1rz2h +rVKIYLfeM2M8lVzSL1V0aJ+POcruTRsmlrFT5f7na/5YFt5N+5Z5fzixCLr1MK2w +4gFw+KhN7CAhKGzHq3NBdWpRFFMR53hyeYsb1vvwFu07JTRh+NbaePk/sk07WtCo +u2w6pD7xlayTAWcR9WRBv7c3lDejN80U8DONb8fLwtI5oIrkSuwOqvmlDOeFpKiT +MwTB6oC81Ar39P0R53247w7u9plhPUrmDn/A5KphW633UvgbkH6VmB4Isiw= +-----END CERTIFICATE----- diff --git a/client/testdata/gitalykey.pem b/client/testdata/gitalykey.pem new file mode 100644 index 000000000..8df87e633 --- /dev/null +++ b/client/testdata/gitalykey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +eM9aMpgdAlLOJrfZHS3kK3968ZclB4GB +-----END PRIVATE KEY----- |