Commit 0b9148bf authored by Jacob Vosmaer's avatar Jacob Vosmaer

Support insecure TCP connections to Gitaly

parent c1326899
...@@ -89,8 +89,10 @@ type Response struct { ...@@ -89,8 +89,10 @@ type Response struct {
Entry string `json:"entry"` Entry string `json:"entry"`
// Used to communicate terminal session details // Used to communicate terminal session details
Terminal *TerminalSettings Terminal *TerminalSettings
// Path to Gitaly Socket // Path to Gitaly Socket (deprecated in favor of GitalyAddress)
GitalySocketPath string GitalySocketPath string
// GitalyAddress is a unix:// or tcp:// address to reach a Gitaly service on
GitalyAddress string
// Repository object for making gRPC requests to Gitaly. This will // Repository object for making gRPC requests to Gitaly. This will
// eventually replace the RepoPath field. // eventually replace the RepoPath field.
Repository pb.Repository Repository pb.Repository
...@@ -216,11 +218,16 @@ func (api *API) PreAuthorize(suffix string, r *http.Request) (httpResponse *http ...@@ -216,11 +218,16 @@ func (api *API) PreAuthorize(suffix string, r *http.Request) (httpResponse *http
// This is for backwards compatiblity, can be depracated when Rails // This is for backwards compatiblity, can be depracated when Rails
// always sends a 'Repository' message. For the time being we cannot // always sends a 'Repository' message. For the time being we cannot
// count on this, so we put some minimal data in the Repository struct. // count on this, so we put some minimal data in the Repository struct.
if authResponse.Repository.Path == "" { if authResponse.Repository.Path == "" {
authResponse.Repository.Path = authResponse.RepoPath authResponse.Repository.Path = authResponse.RepoPath
} }
if socketPath := authResponse.GitalySocketPath; socketPath != "" && authResponse.GitalyAddress == "" {
// We are transitioning away from the GitalySocketPath response field.
// Until all the new code is in place, keep backwards compatibility.
authResponse.GitalyAddress = "unix://" + socketPath
}
return httpResponse, authResponse, nil return httpResponse, authResponse, nil
} }
......
...@@ -29,7 +29,7 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) ...@@ -29,7 +29,7 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
var err error var err error
if a.GitalySocketPath == "" { if a.GitalyAddress == "" {
err = handleGetInfoRefsLocally(w, a, rpc) err = handleGetInfoRefsLocally(w, a, rpc)
} else { } else {
err = handleGetInfoRefsWithGitaly(w, a, rpc) err = handleGetInfoRefsWithGitaly(w, a, rpc)
...@@ -62,7 +62,7 @@ func handleGetInfoRefsLocally(w http.ResponseWriter, a *api.Response, rpc string ...@@ -62,7 +62,7 @@ func handleGetInfoRefsLocally(w http.ResponseWriter, a *api.Response, rpc string
} }
func handleGetInfoRefsWithGitaly(w http.ResponseWriter, a *api.Response, rpc string) error { func handleGetInfoRefsWithGitaly(w http.ResponseWriter, a *api.Response, rpc string) error {
smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalySocketPath) smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalyAddress)
if err != nil { if err != nil {
return fmt.Errorf("GetInfoRefsHandler: %v", err) return fmt.Errorf("GetInfoRefsHandler: %v", err)
} }
......
...@@ -20,7 +20,7 @@ func handleReceivePack(w *GitHttpResponseWriter, r *http.Request, a *api.Respons ...@@ -20,7 +20,7 @@ func handleReceivePack(w *GitHttpResponseWriter, r *http.Request, a *api.Respons
defer cw.Flush() defer cw.Flush()
var err error var err error
if a.GitalySocketPath == "" { if a.GitalyAddress == "" {
err = handleReceivePackLocally(a, r, cr, cw, action) err = handleReceivePackLocally(a, r, cr, cw, action)
} else { } else {
err = handleReceivePackWithGitaly(a, cr, cw) err = handleReceivePackWithGitaly(a, cr, cw)
...@@ -46,7 +46,7 @@ func handleReceivePackLocally(a *api.Response, r *http.Request, stdin io.Reader, ...@@ -46,7 +46,7 @@ func handleReceivePackLocally(a *api.Response, r *http.Request, stdin io.Reader,
} }
func handleReceivePackWithGitaly(a *api.Response, clientRequest io.Reader, clientResponse io.Writer) error { func handleReceivePackWithGitaly(a *api.Response, clientRequest io.Reader, clientResponse io.Writer) error {
smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalySocketPath) smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalyAddress)
if err != nil { if err != nil {
return fmt.Errorf("smarthttp.ReceivePack: %v", err) return fmt.Errorf("smarthttp.ReceivePack: %v", err)
} }
......
...@@ -27,7 +27,7 @@ func handleUploadPack(w *GitHttpResponseWriter, r *http.Request, a *api.Response ...@@ -27,7 +27,7 @@ func handleUploadPack(w *GitHttpResponseWriter, r *http.Request, a *api.Response
action := getService(r) action := getService(r)
writePostRPCHeader(w, action) writePostRPCHeader(w, action)
if a.GitalySocketPath == "" { if a.GitalyAddress == "" {
err = handleUploadPackLocally(a, r, buffer, w, action) err = handleUploadPackLocally(a, r, buffer, w, action)
} else { } else {
err = handleUploadPackWithGitaly(a, buffer, w) err = handleUploadPackWithGitaly(a, buffer, w)
...@@ -58,7 +58,7 @@ func handleUploadPackLocally(a *api.Response, r *http.Request, stdin *os.File, s ...@@ -58,7 +58,7 @@ func handleUploadPackLocally(a *api.Response, r *http.Request, stdin *os.File, s
} }
func handleUploadPackWithGitaly(a *api.Response, clientRequest io.Reader, clientResponse io.Writer) error { func handleUploadPackWithGitaly(a *api.Response, clientRequest io.Reader, clientResponse io.Writer) error {
smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalySocketPath) smarthttp, err := gitaly.NewSmartHTTPClient(a.GitalyAddress)
if err != nil { if err != nil {
return fmt.Errorf("smarthttp.UploadPack: %v", err) return fmt.Errorf("smarthttp.UploadPack: %v", err)
} }
......
package gitaly package gitaly
import ( import (
"fmt"
"net" "net"
"net/url"
"strings"
"sync" "sync"
"time" "time"
pb "gitlab.com/gitlab-org/gitaly-proto/go" pb "gitlab.com/gitlab-org/gitaly-proto/go"
"google.golang.org/grpc" "google.golang.org/grpc"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/badgateway"
) )
type connectionsCache struct { type connectionsCache struct {
...@@ -20,8 +21,8 @@ var cache = connectionsCache{ ...@@ -20,8 +21,8 @@ var cache = connectionsCache{
connections: make(map[string]*grpc.ClientConn), connections: make(map[string]*grpc.ClientConn),
} }
func NewSmartHTTPClient(socketPath string) (*SmartHTTPClient, error) { func NewSmartHTTPClient(address string) (*SmartHTTPClient, error) {
conn, err := getOrCreateConnection(socketPath) conn, err := getOrCreateConnection(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -29,20 +30,20 @@ func NewSmartHTTPClient(socketPath string) (*SmartHTTPClient, error) { ...@@ -29,20 +30,20 @@ func NewSmartHTTPClient(socketPath string) (*SmartHTTPClient, error) {
return &SmartHTTPClient{grpcClient}, nil return &SmartHTTPClient{grpcClient}, nil
} }
func getOrCreateConnection(socketPath string) (*grpc.ClientConn, error) { func getOrCreateConnection(address string) (*grpc.ClientConn, error) {
cache.Lock() cache.Lock()
defer cache.Unlock() defer cache.Unlock()
if conn := cache.connections[socketPath]; conn != nil { if conn := cache.connections[address]; conn != nil {
return conn, nil return conn, nil
} }
conn, err := newConnection(socketPath) conn, err := newConnection(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cache.connections[socketPath] = conn cache.connections[address] = conn
return conn, nil return conn, nil
} }
...@@ -56,17 +57,48 @@ func CloseConnections() { ...@@ -56,17 +57,48 @@ func CloseConnections() {
} }
} }
func newConnection(socketPath string) (*grpc.ClientConn, error) { func newConnection(rawAddress string) (*grpc.ClientConn, error) {
network, addr, err := parseAddress(rawAddress)
if err != nil {
return nil, err
}
connOpts := []grpc.DialOption{ connOpts := []grpc.DialOption{
grpc.WithInsecure(), // Since we're connecting to Gitaly over UNIX, we don't need to use TLS credentials. grpc.WithInsecure(), // Since we're connecting to Gitaly over UNIX, we don't need to use TLS credentials.
grpc.WithDialer(func(addr string, _ time.Duration) (net.Conn, error) { grpc.WithDialer(func(a string, _ time.Duration) (net.Conn, error) {
return badgateway.DefaultDialer.Dial("unix", addr) return net.Dial(network, a)
}), }),
} }
conn, err := grpc.Dial(socketPath, connOpts...) conn, err := grpc.Dial(addr, connOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return conn, nil return conn, nil
} }
func parseAddress(rawAddress string) (network, addr string, err error) {
// Parsing unix:// URL's with url.Parse does not give the result we want
// so we do it manually.
for _, prefix := range []string{"unix://", "unix:"} {
if strings.HasPrefix(rawAddress, prefix) {
return "unix", strings.TrimPrefix(rawAddress, prefix), nil
}
}
u, err := url.Parse(rawAddress)
if err != nil {
return "", "", err
}
if u.Scheme != "tcp" {
return "", "", fmt.Errorf("unknown scheme: %q", rawAddress)
}
if u.Host == "" {
return "", "", fmt.Errorf("network tcp requires host: %q", rawAddress)
}
if u.Path != "" {
return "", "", fmt.Errorf("network tcp should have no path: %q", rawAddress)
}
return "tcp", u.Host, nil
}
package gitaly
import (
"testing"
)
func TestParseAddress(t *testing.T) {
testCases := []struct {
raw string
network string
addr string
invalid bool
}{
{raw: "unix:/foo/bar.socket", network: "unix", addr: "/foo/bar.socket"},
{raw: "unix:///foo/bar.socket", network: "unix", addr: "/foo/bar.socket"},
// Mainly for test purposes we explicitly want to support relative paths
{raw: "unix://foo/bar.socket", network: "unix", addr: "foo/bar.socket"},
{raw: "unix:foo/bar.socket", network: "unix", addr: "foo/bar.socket"},
{raw: "tcp://1.2.3.4", network: "tcp", addr: "1.2.3.4"},
{raw: "tcp://1.2.3.4/foo/bar.socket", invalid: true},
{raw: "tcp:///foo/bar.socket", invalid: true},
{raw: "tcp:/foo/bar.socket", invalid: true},
}
for _, tc := range testCases {
network, addr, 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.network != network {
t.Errorf("%v: expected %q, got %q", tc, tc.network, network)
}
if tc.addr != addr {
t.Errorf("%v: expected %q, got %q", tc, tc.addr, addr)
}
}
}
...@@ -604,46 +604,60 @@ func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) { ...@@ -604,46 +604,60 @@ func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t) gitalyServer, socketPath := startGitalyServer(t)
defer gitalyServer.Stop() defer gitalyServer.Stop()
apiResponse.GitalySocketPath = socketPath gitalyAddress := "unix://" + socketPath
ts := testAuthServer(nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL) addressCases := []struct {
defer ws.Close() socketPath string
address string
}{
{socketPath: "/nonexistent,/should/be/ignored", address: gitalyAddress},
{socketPath: socketPath},
}
for _, testCase := range []struct { repoCases := []struct {
repoPath string repoPath string
repository pb.Repository repository pb.Repository
}{ }{
{repoPath: repoPath}, {
{repoPath: repoPath, repository: pb.Repository{Path: repoPath, StorageName: "foobar", RelativePath: "baz.git"}}, repoPath: repoPath,
} { },
func() { {
apiResponse.RepoPath = testCase.repoPath repoPath: repoPath,
apiResponse.Repository = testCase.repository repository: pb.Repository{Path: repoPath, StorageName: "foobar", RelativePath: "baz.git"},
apiResponse.GitalySocketPath = socketPath },
ts := testAuthServer(nil, 200, apiResponse) }
defer ts.Close()
for _, ac := range addressCases {
ws := startWorkhorseServer(ts.URL) for _, rc := range repoCases {
defer ws.Close() func() {
apiResponse.RepoPath = rc.repoPath
resource := "/gitlab-org/gitlab-test.git/info/refs?service=git-upload-pack" apiResponse.Repository = rc.repository
resp, err := http.Get(ws.URL + resource) apiResponse.GitalySocketPath = ac.socketPath
if err != nil { apiResponse.GitalyAddress = ac.address
t.Fatal(err)
} ts := testAuthServer(nil, 200, apiResponse)
defer resp.Body.Close() defer ts.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil { ws := startWorkhorseServer(ts.URL)
t.Error(err) defer ws.Close()
}
resource := "/gitlab-org/gitlab-test.git/info/refs?service=git-upload-pack"
expectedContent := testhelper.GitalyInfoRefsResponseMock resp, err := http.Get(ws.URL + resource)
if !bytes.Equal(responseBody, []byte(expectedContent)) { if err != nil {
t.Errorf("GET %q: Expected %q, got %q", resource, expectedContent, responseBody) t.Fatal(err)
} }
}() defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
}
expectedContent := testhelper.GitalyInfoRefsResponseMock
if !bytes.Equal(responseBody, []byte(expectedContent)) {
t.Errorf("GET %q: Expected %q, got %q", resource, expectedContent, responseBody)
}
}()
}
} }
} }
...@@ -653,7 +667,7 @@ func TestPostReceivePackProxiedToGitalySuccessfully(t *testing.T) { ...@@ -653,7 +667,7 @@ func TestPostReceivePackProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t) gitalyServer, socketPath := startGitalyServer(t)
defer gitalyServer.Stop() defer gitalyServer.Stop()
apiResponse.GitalySocketPath = socketPath apiResponse.GitalyAddress = "unix://" + socketPath
ts := testAuthServer(nil, 200, apiResponse) ts := testAuthServer(nil, 200, apiResponse)
defer ts.Close() defer ts.Close()
...@@ -690,7 +704,7 @@ func TestPostUploadPackProxiedToGitalySuccessfully(t *testing.T) { ...@@ -690,7 +704,7 @@ func TestPostUploadPackProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t) gitalyServer, socketPath := startGitalyServer(t)
defer gitalyServer.Stop() defer gitalyServer.Stop()
apiResponse.GitalySocketPath = socketPath apiResponse.GitalyAddress = "unix://" + socketPath
ts := testAuthServer(nil, 200, apiResponse) ts := testAuthServer(nil, 200, apiResponse)
defer ts.Close() defer ts.Close()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment