aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing
diff options
context:
space:
mode:
authorPaulo Gomes <pjbgf@linux.com>2023-05-04 22:10:19 +0100
committerGitHub <noreply@github.com>2023-05-04 22:10:19 +0100
commit191f4ba946c768221dd914fcf0675572fc36c55d (patch)
tree52c3450287073b3d8f65b2c001f9c7150cc66b9d /plumbing
parentda73c5f950fb399611e3eb608f6ee99f23eb53b5 (diff)
parenta830187d90a6bc36f9466c075ed49076f591efa9 (diff)
downloadgo-git-191f4ba946c768221dd914fcf0675572fc36c55d.tar.gz
Merge pull request #744 from aryan9600/proxy-options
Add support for custom proxy settings
Diffstat (limited to 'plumbing')
-rw-r--r--plumbing/transport/client/client.go32
-rw-r--r--plumbing/transport/common.go31
-rw-r--r--plumbing/transport/http/common.go167
-rw-r--r--plumbing/transport/http/common_test.go54
-rw-r--r--plumbing/transport/http/internal/test/proxy_test.go72
-rw-r--r--plumbing/transport/http/internal/test/test_utils.go43
-rw-r--r--plumbing/transport/http/proxy_test.go119
-rw-r--r--plumbing/transport/http/receive_pack.go2
-rw-r--r--plumbing/transport/http/testdata/certs/server.crt22
-rw-r--r--plumbing/transport/http/testdata/certs/server.key28
-rw-r--r--plumbing/transport/http/transport.go40
-rw-r--r--plumbing/transport/http/upload_pack.go2
-rw-r--r--plumbing/transport/ssh/common.go30
-rw-r--r--plumbing/transport/ssh/internal/test/proxy_test.go113
-rw-r--r--plumbing/transport/ssh/internal/test/test_utils.go83
-rw-r--r--plumbing/transport/ssh/proxy_test.go72
-rw-r--r--plumbing/transport/ssh/upload_pack_test.go3
17 files changed, 854 insertions, 59 deletions
diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go
index 20c3d05..1948c23 100644
--- a/plumbing/transport/client/client.go
+++ b/plumbing/transport/client/client.go
@@ -3,10 +3,7 @@
package client
import (
- "crypto/tls"
- "crypto/x509"
"fmt"
- gohttp "net/http"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/file"
@@ -24,14 +21,6 @@ var Protocols = map[string]transport.Transport{
"file": file.DefaultClient,
}
-var insecureClient = http.NewClient(&gohttp.Client{
- Transport: &gohttp.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- },
- },
-})
-
// InstallProtocol adds or modifies an existing protocol.
func InstallProtocol(scheme string, c transport.Transport) {
if c == nil {
@@ -50,27 +39,6 @@ func NewClient(endpoint *transport.Endpoint) (transport.Transport, error) {
}
func getTransport(endpoint *transport.Endpoint) (transport.Transport, error) {
- if endpoint.Protocol == "https" {
- if endpoint.InsecureSkipTLS {
- return insecureClient, nil
- }
-
- if len(endpoint.CaBundle) != 0 {
- rootCAs, _ := x509.SystemCertPool()
- if rootCAs == nil {
- rootCAs = x509.NewCertPool()
- }
- rootCAs.AppendCertsFromPEM(endpoint.CaBundle)
- return http.NewClient(&gohttp.Client{
- Transport: &gohttp.Transport{
- TLSClientConfig: &tls.Config{
- RootCAs: rootCAs,
- },
- },
- }), nil
- }
- }
-
f, ok := Protocols[endpoint.Protocol]
if !ok {
return nil, fmt.Errorf("unsupported scheme %q", endpoint.Protocol)
diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go
index a2a78f0..89bd3d5 100644
--- a/plumbing/transport/common.go
+++ b/plumbing/transport/common.go
@@ -116,6 +116,37 @@ type Endpoint struct {
InsecureSkipTLS bool
// CaBundle specify additional ca bundle with system cert pool
CaBundle []byte
+ // Proxy provides info required for connecting to a proxy.
+ Proxy ProxyOptions
+}
+
+type ProxyOptions struct {
+ URL string
+ Username string
+ Password string
+}
+
+func (o *ProxyOptions) Validate() error {
+ if o.URL != "" {
+ _, err := url.Parse(o.URL)
+ return err
+ }
+ return nil
+}
+
+func (o *ProxyOptions) FullURL() (*url.URL, error) {
+ proxyURL, err := url.Parse(o.URL)
+ if err != nil {
+ return nil, err
+ }
+ if o.Username != "" {
+ if o.Password != "" {
+ proxyURL.User = url.UserPassword(o.Username, o.Password)
+ } else {
+ proxyURL.User = url.User(o.Username)
+ }
+ }
+ return proxyURL, nil
}
var defaultPorts = map[string]int{
diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go
index d57c0fe..f9b7a0e 100644
--- a/plumbing/transport/http/common.go
+++ b/plumbing/transport/http/common.go
@@ -4,16 +4,22 @@ package http
import (
"bytes"
"context"
+ "crypto/tls"
+ "crypto/x509"
"fmt"
"net"
"net/http"
+ "net/url"
+ "reflect"
"strconv"
"strings"
+ "sync"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/utils/ioutil"
+ "github.com/golang/groupcache/lru"
)
// it requires a bytes.Buffer, because we need to know the length
@@ -74,40 +80,83 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
}
type client struct {
- c *http.Client
+ c *http.Client
+ transports *lru.Cache
+ m sync.RWMutex
}
-// DefaultClient is the default HTTP client, which uses `http.DefaultClient`.
-var DefaultClient = NewClient(nil)
+// ClientOptions holds user configurable options for the client.
+type ClientOptions struct {
+ // CacheMaxEntries is the max no. of entries that the transport objects
+ // cache will hold at any given point of time. It must be a positive integer.
+ // Calling `client.addTransport()` after the cache has reached the specified
+ // size, will result in the least recently used transport getting deleted
+ // before the provided transport is added to the cache.
+ CacheMaxEntries int
+}
+
+var (
+ // defaultTransportCacheSize is the default capacity of the transport objects cache.
+ // Its value is 0 because transport caching is turned off by default and is an
+ // opt-in feature.
+ defaultTransportCacheSize = 0
+
+ // DefaultClient is the default HTTP client, which uses a net/http client configured
+ // with http.DefaultTransport.
+ DefaultClient = NewClient(nil)
+)
// NewClient creates a new client with a custom net/http client.
// See `InstallProtocol` to install and override default http client.
-// Unless a properly initialized client is given, it will fall back into
-// `http.DefaultClient`.
+// If the net/http client is nil or empty, it will use a net/http client configured
+// with http.DefaultTransport.
//
// Note that for HTTP client cannot distinguish between private repositories and
// unexistent repositories on GitHub. So it returns `ErrAuthorizationRequired`
// for both.
func NewClient(c *http.Client) transport.Transport {
if c == nil {
- return &client{http.DefaultClient}
+ c = &http.Client{
+ Transport: http.DefaultTransport,
+ }
}
+ return NewClientWithOptions(c, &ClientOptions{
+ CacheMaxEntries: defaultTransportCacheSize,
+ })
+}
- return &client{
+// NewClientWithOptions returns a new client configured with the provided net/http client
+// and other custom options specific to the client.
+// If the net/http client is nil or empty, it will use a net/http client configured
+// with http.DefaultTransport.
+func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transport {
+ if c == nil {
+ c = &http.Client{
+ Transport: http.DefaultTransport,
+ }
+ }
+ cl := &client{
c: c,
}
+
+ if opts != nil {
+ if opts.CacheMaxEntries > 0 {
+ cl.transports = lru.New(opts.CacheMaxEntries)
+ }
+ }
+ return cl
}
func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.UploadPackSession, error) {
- return newUploadPackSession(c.c, ep, auth)
+ return newUploadPackSession(c, ep, auth)
}
func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
transport.ReceivePackSession, error) {
- return newReceivePackSession(c.c, ep, auth)
+ return newReceivePackSession(c, ep, auth)
}
type session struct {
@@ -117,10 +166,106 @@ type session struct {
advRefs *packp.AdvRefs
}
-func newSession(c *http.Client, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
+func transportWithInsecureTLS(transport *http.Transport) {
+ if transport.TLSClientConfig == nil {
+ transport.TLSClientConfig = &tls.Config{}
+ }
+ transport.TLSClientConfig.InsecureSkipVerify = true
+}
+
+func transportWithCABundle(transport *http.Transport, caBundle []byte) error {
+ rootCAs, err := x509.SystemCertPool()
+ if err != nil {
+ return err
+ }
+ if rootCAs == nil {
+ rootCAs = x509.NewCertPool()
+ }
+ rootCAs.AppendCertsFromPEM(caBundle)
+ if transport.TLSClientConfig == nil {
+ transport.TLSClientConfig = &tls.Config{}
+ }
+ transport.TLSClientConfig.RootCAs = rootCAs
+ return nil
+}
+
+func transportWithProxy(transport *http.Transport, proxyURL *url.URL) {
+ transport.Proxy = http.ProxyURL(proxyURL)
+}
+
+func configureTransport(transport *http.Transport, ep *transport.Endpoint) error {
+ if len(ep.CaBundle) > 0 {
+ if err := transportWithCABundle(transport, ep.CaBundle); err != nil {
+ return err
+ }
+ }
+ if ep.InsecureSkipTLS {
+ transportWithInsecureTLS(transport)
+ }
+
+ if ep.Proxy.URL != "" {
+ proxyURL, err := ep.Proxy.FullURL()
+ if err != nil {
+ return err
+ }
+ transportWithProxy(transport, proxyURL)
+ }
+ return nil
+}
+
+func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
+ var httpClient *http.Client
+
+ // We need to configure the http transport if there are transport specific
+ // options present in the endpoint.
+ if len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" {
+ var transport *http.Transport
+ // if the client wasn't configured to have a cache for transports then just configure
+ // the transport and use it directly, otherwise try to use the cache.
+ if c.transports == nil {
+ tr, ok := c.c.Transport.(*http.Transport)
+ if !ok {
+ return nil, fmt.Errorf("expected underlying client transport to be of type: %s; got: %s",
+ reflect.TypeOf(transport), reflect.TypeOf(c.c.Transport))
+ }
+
+ transport = tr.Clone()
+ configureTransport(transport, ep)
+ } else {
+ transportOpts := transportOptions{
+ caBundle: string(ep.CaBundle),
+ insecureSkipTLS: ep.InsecureSkipTLS,
+ }
+ if ep.Proxy.URL != "" {
+ proxyURL, err := ep.Proxy.FullURL()
+ if err != nil {
+ return nil, err
+ }
+ transportOpts.proxyURL = *proxyURL
+ }
+ var found bool
+ transport, found = c.fetchTransport(transportOpts)
+
+ if !found {
+ transport = c.c.Transport.(*http.Transport).Clone()
+ configureTransport(transport, ep)
+ c.addTransport(transportOpts, transport)
+ }
+ }
+
+ httpClient = &http.Client{
+ Transport: transport,
+ CheckRedirect: c.c.CheckRedirect,
+ Jar: c.c.Jar,
+ Timeout: c.c.Timeout,
+ }
+ } else {
+ httpClient = c.c
+ }
+
s := &session{
auth: basicAuthFromEndpoint(ep),
- client: c,
+ client: httpClient,
endpoint: ep,
}
if auth != nil {
diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go
index 4122e62..41188e6 100644
--- a/plumbing/transport/http/common_test.go
+++ b/plumbing/transport/http/common_test.go
@@ -91,6 +91,60 @@ func (s *ClientSuite) TestNewHTTPError40x(c *C) {
"unexpected client error.*")
}
+func (s *ClientSuite) Test_newSession(c *C) {
+ cl := NewClientWithOptions(nil, &ClientOptions{
+ CacheMaxEntries: 2,
+ }).(*client)
+
+ insecureEP := s.Endpoint
+ insecureEP.InsecureSkipTLS = true
+ session, err := newSession(cl, insecureEP, nil)
+ c.Assert(err, IsNil)
+
+ sessionTransport := session.client.Transport.(*http.Transport)
+ c.Assert(sessionTransport.TLSClientConfig.InsecureSkipVerify, Equals, true)
+ t, ok := cl.fetchTransport(transportOptions{
+ insecureSkipTLS: true,
+ })
+ // transport should be cached.
+ c.Assert(ok, Equals, true)
+ // cached transport should be the one that's used.
+ c.Assert(sessionTransport, Equals, t)
+
+ caEndpoint := insecureEP
+ caEndpoint.CaBundle = []byte("this is the way")
+ session, err = newSession(cl, caEndpoint, nil)
+ c.Assert(err, IsNil)
+
+ sessionTransport = session.client.Transport.(*http.Transport)
+ c.Assert(sessionTransport.TLSClientConfig.InsecureSkipVerify, Equals, true)
+ c.Assert(sessionTransport.TLSClientConfig.RootCAs, NotNil)
+ t, ok = cl.fetchTransport(transportOptions{
+ insecureSkipTLS: true,
+ caBundle: "this is the way",
+ })
+ // transport should be cached.
+ c.Assert(ok, Equals, true)
+ // cached transport should be the one that's used.
+ c.Assert(sessionTransport, Equals, t)
+
+ session, err = newSession(cl, caEndpoint, nil)
+ c.Assert(err, IsNil)
+ sessionTransport = session.client.Transport.(*http.Transport)
+ // transport that's going to be used should be cached already.
+ c.Assert(sessionTransport, Equals, t)
+ // no new transport got cached.
+ c.Assert(cl.transports.Len(), Equals, 2)
+
+ // if the cache does not exist, the transport should still be correctly configured.
+ cl.transports = nil
+ session, err = newSession(cl, insecureEP, nil)
+ c.Assert(err, IsNil)
+
+ sessionTransport = session.client.Transport.(*http.Transport)
+ c.Assert(sessionTransport.TLSClientConfig.InsecureSkipVerify, Equals, true)
+}
+
func (s *ClientSuite) testNewHTTPError(c *C, code int, msg string) {
req, _ := http.NewRequest("GET", "foo", nil)
res := &http.Response{
diff --git a/plumbing/transport/http/internal/test/proxy_test.go b/plumbing/transport/http/internal/test/proxy_test.go
new file mode 100644
index 0000000..6ae2943
--- /dev/null
+++ b/plumbing/transport/http/internal/test/proxy_test.go
@@ -0,0 +1,72 @@
+package test
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ nethttp "net/http"
+ "os"
+ "sync/atomic"
+ "testing"
+
+ "github.com/elazarl/goproxy"
+
+ "github.com/go-git/go-git/v5/plumbing/transport"
+ "github.com/go-git/go-git/v5/plumbing/transport/http"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up gocheck into the "go test" runner.
+func Test(t *testing.T) { TestingT(t) }
+
+type ProxySuite struct{}
+
+var _ = Suite(&ProxySuite{})
+
+var proxiedRequests int32
+
+// This test tests proxy support via an env var, i.e. `HTTPS_PROXY`.
+// Its located in a separate package because golang caches the value
+// of proxy env vars leading to misleading/unexpected test results.
+func (s *ProxySuite) TestAdvertisedReferences(c *C) {
+ proxy := goproxy.NewProxyHttpServer()
+ proxy.Verbose = true
+ SetupHTTPSProxy(proxy, &proxiedRequests)
+ httpsListener, err := net.Listen("tcp", ":0")
+ c.Assert(err, IsNil)
+ defer httpsListener.Close()
+ httpProxyAddr := fmt.Sprintf("localhost:%d", httpsListener.Addr().(*net.TCPAddr).Port)
+
+ proxyServer := nethttp.Server{
+ Addr: httpProxyAddr,
+ Handler: proxy,
+ // Due to how golang manages http/2 when provided with custom TLS config,
+ // servers and clients running in the same process leads to issues.
+ // Ref: https://github.com/golang/go/issues/21336
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"http/1.1"},
+ },
+ }
+ go proxyServer.ServeTLS(httpsListener, "../../testdata/certs/server.crt", "../../testdata/certs/server.key")
+ defer proxyServer.Close()
+ os.Setenv("HTTPS_PROXY", fmt.Sprintf("https://user:pass@%s", httpProxyAddr))
+ defer os.Unsetenv("HTTPS_PROXY")
+
+ endpoint, err := transport.NewEndpoint("https://github.com/git-fixtures/basic.git")
+ c.Assert(err, IsNil)
+ endpoint.InsecureSkipTLS = true
+
+ client := http.DefaultClient
+ session, err := client.NewUploadPackSession(endpoint, nil)
+ c.Assert(err, IsNil)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ info, err := session.AdvertisedReferencesContext(ctx)
+ c.Assert(err, IsNil)
+ c.Assert(info, NotNil)
+ proxyUsed := atomic.LoadInt32(&proxiedRequests) > 0
+ c.Assert(proxyUsed, Equals, true)
+}
diff --git a/plumbing/transport/http/internal/test/test_utils.go b/plumbing/transport/http/internal/test/test_utils.go
new file mode 100644
index 0000000..6665fb3
--- /dev/null
+++ b/plumbing/transport/http/internal/test/test_utils.go
@@ -0,0 +1,43 @@
+package test
+
+import (
+ "encoding/base64"
+ "strings"
+ "sync/atomic"
+
+ "github.com/elazarl/goproxy"
+)
+
+func SetupHTTPSProxy(proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) {
+ var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
+ if strings.Contains(host, "github.com") {
+ user, pass, _ := ParseBasicAuth(ctx.Req.Header.Get("Proxy-Authorization"))
+ if user != "user" || pass != "pass" {
+ return goproxy.RejectConnect, host
+ }
+ atomic.AddInt32(proxiedRequests, 1)
+ return goproxy.OkConnect, host
+ }
+ // Reject if it isn't our request.
+ return goproxy.RejectConnect, host
+ }
+ proxy.OnRequest().HandleConnect(proxyHandler)
+}
+
+// adapted from https://github.com/golang/go/blob/2ef70d9d0f98832c8103a7968b195e560a8bb262/src/net/http/request.go#L959
+func ParseBasicAuth(auth string) (username, password string, ok bool) {
+ const prefix = "Basic "
+ if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
+ return "", "", false
+ }
+ c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
+ if err != nil {
+ return "", "", false
+ }
+ cs := string(c)
+ username, password, ok = strings.Cut(cs, ":")
+ if !ok {
+ return "", "", false
+ }
+ return username, password, true
+}
diff --git a/plumbing/transport/http/proxy_test.go b/plumbing/transport/http/proxy_test.go
new file mode 100644
index 0000000..f3024da
--- /dev/null
+++ b/plumbing/transport/http/proxy_test.go
@@ -0,0 +1,119 @@
+package http
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+ "sync/atomic"
+
+ "github.com/elazarl/goproxy"
+ fixtures "github.com/go-git/go-git-fixtures/v4"
+ "github.com/go-git/go-git/v5/plumbing/transport"
+ "github.com/go-git/go-git/v5/plumbing/transport/http/internal/test"
+
+ . "gopkg.in/check.v1"
+)
+
+type ProxySuite struct {
+ u UploadPackSuite
+ fixtures.Suite
+}
+
+var _ = Suite(&ProxySuite{})
+
+var proxiedRequests int32
+
+func (s *ProxySuite) TestAdvertisedReferences(c *C) {
+ s.u.SetUpTest(c)
+ proxy := goproxy.NewProxyHttpServer()
+ proxy.Verbose = true
+ setupHTTPProxy(proxy, &proxiedRequests)
+ httpListener, err := net.Listen("tcp", ":0")
+ c.Assert(err, IsNil)
+ defer httpListener.Close()
+
+ httpProxyAddr := fmt.Sprintf("http://localhost:%d", httpListener.Addr().(*net.TCPAddr).Port)
+ proxyServer := http.Server{
+ Addr: httpProxyAddr,
+ Handler: proxy,
+ }
+ go proxyServer.Serve(httpListener)
+ defer proxyServer.Close()
+
+ endpoint := s.u.prepareRepository(c, fixtures.Basic().One(), "basic.git")
+ endpoint.Proxy = transport.ProxyOptions{
+ URL: httpProxyAddr,
+ Username: "user",
+ Password: "pass",
+ }
+
+ s.u.Client = NewClient(nil)
+ session, err := s.u.Client.NewUploadPackSession(endpoint, nil)
+ c.Assert(err, IsNil)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ info, err := session.AdvertisedReferencesContext(ctx)
+ c.Assert(err, IsNil)
+ c.Assert(info, NotNil)
+ proxyUsed := atomic.LoadInt32(&proxiedRequests) > 0
+ c.Assert(proxyUsed, Equals, true)
+
+ atomic.StoreInt32(&proxiedRequests, 0)
+ test.SetupHTTPSProxy(proxy, &proxiedRequests)
+ httpsListener, err := net.Listen("tcp", ":0")
+ c.Assert(err, IsNil)
+ defer httpsListener.Close()
+ httpsProxyAddr := fmt.Sprintf("https://localhost:%d", httpsListener.Addr().(*net.TCPAddr).Port)
+
+ tlsProxyServer := http.Server{
+ Addr: httpsProxyAddr,
+ Handler: proxy,
+ // Due to how golang manages http/2 when provided with custom TLS config,
+ // servers and clients running in the same process leads to issues.
+ // Ref: https://github.com/golang/go/issues/21336
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"http/1.1"},
+ },
+ }
+ go tlsProxyServer.ServeTLS(httpsListener, "testdata/certs/server.crt", "testdata/certs/server.key")
+ defer tlsProxyServer.Close()
+
+ endpoint, err = transport.NewEndpoint("https://github.com/git-fixtures/basic.git")
+ c.Assert(err, IsNil)
+ endpoint.Proxy = transport.ProxyOptions{
+ URL: httpsProxyAddr,
+ Username: "user",
+ Password: "pass",
+ }
+ endpoint.InsecureSkipTLS = true
+
+ session, err = s.u.Client.NewUploadPackSession(endpoint, nil)
+ c.Assert(err, IsNil)
+
+ info, err = session.AdvertisedReferencesContext(ctx)
+ c.Assert(err, IsNil)
+ c.Assert(info, NotNil)
+ proxyUsed = atomic.LoadInt32(&proxiedRequests) > 0
+ c.Assert(proxyUsed, Equals, true)
+}
+
+func setupHTTPProxy(proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) {
+ // The request is being forwarded to the local test git server in this handler.
+ var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
+ if strings.Contains(req.Host, "localhost") {
+ user, pass, _ := test.ParseBasicAuth(req.Header.Get("Proxy-Authorization"))
+ if user != "user" || pass != "pass" {
+ return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusUnauthorized, "")
+ }
+ atomic.AddInt32(proxiedRequests, 1)
+ return req, nil
+ }
+ // Reject if it isn't our request.
+ return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
+ }
+ proxy.OnRequest().Do(proxyHandler)
+}
diff --git a/plumbing/transport/http/receive_pack.go b/plumbing/transport/http/receive_pack.go
index 4d14ff2..4387ecf 100644
--- a/plumbing/transport/http/receive_pack.go
+++ b/plumbing/transport/http/receive_pack.go
@@ -19,7 +19,7 @@ type rpSession struct {
*session
}
-func newReceivePackSession(c *http.Client, ep *transport.Endpoint, auth transport.AuthMethod) (transport.ReceivePackSession, error) {
+func newReceivePackSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (transport.ReceivePackSession, error) {
s, err := newSession(c, ep, auth)
return &rpSession{s}, err
}
diff --git a/plumbing/transport/http/testdata/certs/server.crt b/plumbing/transport/http/testdata/certs/server.crt
new file mode 100644
index 0000000..9bdec2c
--- /dev/null
+++ b/plumbing/transport/http/testdata/certs/server.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIUWcuzUyG3EfGsXVUH0BAmnCJyNHswDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
+DTIzMDMwNzA3MTgwNloXDTI0MDMwNjA3MTgwNlowWTELMAkGA1UEBhMCQVUxEzAR
+BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
+IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAvyKX6vJXt1u+WBfBNJByFDAb7msdsk6SiPFlX5uyilaWmlRxvLo1
+GZMjjuQbs4wU6BAoZcgiELnsC9GSyxgrhk7NIW3ud/QD7s8ZxETxFLb0ur6tJj7/
+ETEcU/AKSl1FpeJbGHqGipYp5+0GU0zPDxRYqC2N3+fcGZPQbBwxb1f+MrBjWutb
+3eNYTLdPH3W7RUqbunC1KZRJ8XOcU5XZ4qEaMkZYdz1QItxwPnpPuSZs53ga3TDF
+zclpQcT8OH2JNwSI6bwlwFJ0Es06manw7XHmgd8anhix9FdsQYaTOW4kqh1iKQ/P
+jPG50bdTUEqlOsaa+9av/qf+90npzt3xqQIDAQABo1MwUTAdBgNVHQ4EFgQUqTqb
+q+jiJVgwftQS+YLcQWnvTuAwHwYDVR0jBBgwFoAUqTqbq+jiJVgwftQS+YLcQWnv
+TuAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVUaFSikxyCy1
+4P/ZZgeuR7vEJ5vWBxKPw/jFNZUFWy2Ag32w1BhrDwoYoc1Awg76QF2TqBQAhFNm
+ek9aE+L83P/R2UhE9+LHnzwdMXt9HYOI1grONk2z3lMI1y4FCJBxHfGyC/XMoNgZ
+qP7UdLgLGTIMN3O1Fww416Hn8BHzxN4o5ZEHJZ6QPMuN8OLk9oVu3yQIq/QcmSDH
+GT2RiwT5IJWMUKK1UrV+y3/T9FwW2qqu+LX+coxjk7HgDWy3y66V9ahLBt8kONcr
+qK0zutoQ5WPSmvnD2Nr0LVLGXEd7hbQNO7bgjO2YOBtnagUQJt72i/OmvZv8Mfnp
+Bu6Qgl5hDw==
+-----END CERTIFICATE-----
diff --git a/plumbing/transport/http/testdata/certs/server.key b/plumbing/transport/http/testdata/certs/server.key
new file mode 100644
index 0000000..9a0cd8f
--- /dev/null
+++ b/plumbing/transport/http/testdata/certs/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/Ipfq8le3W75Y
+F8E0kHIUMBvuax2yTpKI8WVfm7KKVpaaVHG8ujUZkyOO5BuzjBToEChlyCIQuewL
+0ZLLGCuGTs0hbe539APuzxnERPEUtvS6vq0mPv8RMRxT8ApKXUWl4lsYeoaKlinn
+7QZTTM8PFFioLY3f59wZk9BsHDFvV/4ysGNa61vd41hMt08fdbtFSpu6cLUplEnx
+c5xTldnioRoyRlh3PVAi3HA+ek+5JmzneBrdMMXNyWlBxPw4fYk3BIjpvCXAUnQS
+zTqZqfDtceaB3xqeGLH0V2xBhpM5biSqHWIpD8+M8bnRt1NQSqU6xpr71q/+p/73
+SenO3fGpAgMBAAECggEAQUjenQhzv5Rdmpdajcq8vHqGP9Rki0/dK1tQpex3elsD
+C+nGA5GSq46feaIeeCBjz7QdKE7Im+/1WUAXJLm3vCNUW5PB/UTixwIEKg7mTY4E
+X3jbiZHA661boKv/x9C+BmAff2fyZonN/ILwQymcG+l2MtOEfzMh8baUXSjwFbhg
+B08u4iXjee0y9I0CGMYWfasHLOIuhACCFKtqnvdQp8B82g8eSPhme5IjfPP8KZVr
+00n6z8m00HVk6/yYJ8pVZ82j3T+wH6IqvlvaC320sbto8YXV6i8GWHaJumzU4/9s
+IRm4459E+NmNcLNY/TCu89zsfrgNirN+qFfvJIOTxQKBgQDtME8s4UP0MhGuJ2lD
+1HD64fAxMC6Xp/QSzY91Yn79UNssUUV+IwjuUnLIz3U8DBs/QETLm7CkNtI7h5m5
+dBdeBBzCRGxhe8WqRfvceu4s0zr08ZkDaKLjFsBSnBsXZhKAAuRqBjnGAoAiKgVa
+WpEAug00ThhQjipSY9tO9NSBawKBgQDOSz+8m2HJFktEdSctKIB9DesqlAg7YCyy
+dHzywP0/r7wEvsCN7xPgCT5g8JBkRaFvLLKgw7gMKAUx8V2iwizEoDCAs/pbTWji
+uZwPC8lWtbkpBMQIaP4Wap+GyFQJKv1/qZduwpkwkj+ok+m3WwIW55VFGyLn3XGG
+VcLZm83aOwKBgQDXXI/nXjqHVZb8HEjWD+Ttx4yB/Q+xIAzbrc3edap8c5guKzUA
+DOulCTOz5bq65PsweTh970V6NVS6PKt12lUFRpKeSeZmtS2LJ7RCQ1RTWxAjK+MV
+V0LfEt9ZouhuXH3bwcSICFMY2VhirOjjW2xhzo0Cuw4UxqDi4kxU6rSxNQKBgQCI
+sn5KmV/jot0/QK40E0mJFEcHkM4foiwcGGqPZWiq4eUh89CefJTb+OQX0nCrsSQ3
+ChRXyTlU/NPsczcL2cVWiZt6PUihZZsh2cJaigHhbkuCrcDEneX4rrCE3IwrAwy1
+oohRAawG7nI2X8UYFbs9uDlGcKPhpvBKBtw13DM87wKBgE8fOiFoTph//6piU7dV
+pN33UfhPcAFwsIzxAH6Ljo6BYx2hfPRCxI2g0wchk6ydbDecLgMwVgugdJZ6+tRf
+P+YV3wEwPcWOvWby3+EmJh0cXUTl6ZMA+eR4pvCi6kf2xJf9dRmEeNNhOuzn9Y0J
+cT9yhBFG4iejKP0iTwET1JKY
+-----END PRIVATE KEY-----
diff --git a/plumbing/transport/http/transport.go b/plumbing/transport/http/transport.go
new file mode 100644
index 0000000..052f3c8
--- /dev/null
+++ b/plumbing/transport/http/transport.go
@@ -0,0 +1,40 @@
+package http
+
+import (
+ "net/http"
+ "net/url"
+)
+
+// transportOptions contains transport specific configuration.
+type transportOptions struct {
+ insecureSkipTLS bool
+ // []byte is not comparable.
+ caBundle string
+ proxyURL url.URL
+}
+
+func (c *client) addTransport(opts transportOptions, transport *http.Transport) {
+ c.m.Lock()
+ c.transports.Add(opts, transport)
+ c.m.Unlock()
+}
+
+func (c *client) removeTransport(opts transportOptions) {
+ c.m.Lock()
+ c.transports.Remove(opts)
+ c.m.Unlock()
+}
+
+func (c *client) fetchTransport(opts transportOptions) (*http.Transport, bool) {
+ c.m.RLock()
+ t, ok := c.transports.Get(opts)
+ c.m.RUnlock()
+ if !ok {
+ return nil, false
+ }
+ transport, ok := t.(*http.Transport)
+ if !ok {
+ return nil, false
+ }
+ return transport, true
+}
diff --git a/plumbing/transport/http/upload_pack.go b/plumbing/transport/http/upload_pack.go
index e735b3d..4f85145 100644
--- a/plumbing/transport/http/upload_pack.go
+++ b/plumbing/transport/http/upload_pack.go
@@ -19,7 +19,7 @@ type upSession struct {
*session
}
-func newUploadPackSession(c *http.Client, ep *transport.Endpoint, auth transport.AuthMethod) (transport.UploadPackSession, error) {
+func newUploadPackSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (transport.UploadPackSession, error) {
s, err := newSession(c, ep, auth)
return &upSession{s}, err
}
diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go
index e06958a..6617d9b 100644
--- a/plumbing/transport/ssh/common.go
+++ b/plumbing/transport/ssh/common.go
@@ -4,6 +4,7 @@ package ssh
import (
"context"
"fmt"
+ "net"
"reflect"
"strconv"
"strings"
@@ -139,7 +140,7 @@ func (c *command) connect() error {
overrideConfig(c.config, config)
- c.client, err = dial("tcp", hostWithPort, config)
+ c.client, err = dial("tcp", hostWithPort, c.endpoint.Proxy, config)
if err != nil {
return err
}
@@ -154,7 +155,7 @@ func (c *command) connect() error {
return nil
}
-func dial(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
+func dial(network, addr string, proxyOpts transport.ProxyOptions, config *ssh.ClientConfig) (*ssh.Client, error) {
var (
ctx = context.Background()
cancel context.CancelFunc
@@ -166,10 +167,33 @@ func dial(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
}
defer cancel()
- conn, err := proxy.Dial(ctx, network, addr)
+ var conn net.Conn
+ var err error
+
+ if proxyOpts.URL != "" {
+ proxyUrl, err := proxyOpts.FullURL()
+ if err != nil {
+ return nil, err
+ }
+ dialer, err := proxy.FromURL(proxyUrl, proxy.Direct)
+ if err != nil {
+ return nil, err
+ }
+
+ // Try to use a ContextDialer, but fall back to a Dialer if that goes south.
+ ctxDialer, ok := dialer.(proxy.ContextDialer)
+ if !ok {
+ return nil, fmt.Errorf("expected ssh proxy dialer to be of type %s; got %s",
+ reflect.TypeOf(ctxDialer), reflect.TypeOf(dialer))
+ }
+ conn, err = ctxDialer.DialContext(ctx, "tcp", addr)
+ } else {
+ conn, err = proxy.Dial(ctx, network, addr)
+ }
if err != nil {
return nil, err
}
+
c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
if err != nil {
return nil, err
diff --git a/plumbing/transport/ssh/internal/test/proxy_test.go b/plumbing/transport/ssh/internal/test/proxy_test.go
new file mode 100644
index 0000000..8baac2b
--- /dev/null
+++ b/plumbing/transport/ssh/internal/test/proxy_test.go
@@ -0,0 +1,113 @@
+package test
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+
+ "github.com/armon/go-socks5"
+ "github.com/gliderlabs/ssh"
+ "github.com/go-git/go-git/v5/plumbing/transport"
+ ggssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
+
+ fixtures "github.com/go-git/go-git-fixtures/v4"
+ stdssh "golang.org/x/crypto/ssh"
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ProxyEnvSuite struct {
+ fixtures.Suite
+ port int
+ base string
+}
+
+var _ = Suite(&ProxyEnvSuite{})
+
+var socksProxiedRequests int32
+
+// This test tests proxy support via an env var, i.e. `ALL_PROXY`.
+// Its located in a separate package because golang caches the value
+// of proxy env vars leading to misleading/unexpected test results.
+func (s *ProxyEnvSuite) TestCommand(c *C) {
+ socksListener, err := net.Listen("tcp", "localhost:0")
+ c.Assert(err, IsNil)
+
+ socksServer, err := socks5.New(&socks5.Config{
+ Rules: TestProxyRule{},
+ })
+ c.Assert(err, IsNil)
+ go func() {
+ socksServer.Serve(socksListener)
+ }()
+ socksProxyAddr := fmt.Sprintf("socks5://localhost:%d", socksListener.Addr().(*net.TCPAddr).Port)
+ os.Setenv("ALL_PROXY", socksProxyAddr)
+ defer os.Unsetenv("ALL_PROXY")
+
+ sshListener, err := net.Listen("tcp", "localhost:0")
+ c.Assert(err, IsNil)
+ sshServer := &ssh.Server{Handler: HandlerSSH}
+ go func() {
+ log.Fatal(sshServer.Serve(sshListener))
+ }()
+
+ s.port = sshListener.Addr().(*net.TCPAddr).Port
+ s.base, err = ioutil.TempDir(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.port))
+ c.Assert(err, IsNil)
+
+ ggssh.DefaultAuthBuilder = func(user string) (ggssh.AuthMethod, error) {
+ return &ggssh.Password{User: user}, nil
+ }
+
+ ep := s.prepareRepository(c, fixtures.Basic().One(), "basic.git")
+ c.Assert(err, IsNil)
+
+ client := ggssh.NewClient(&stdssh.ClientConfig{
+ HostKeyCallback: stdssh.InsecureIgnoreHostKey(),
+ })
+ r, err := client.NewUploadPackSession(ep, nil)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info, NotNil)
+ proxyUsed := atomic.LoadInt32(&socksProxiedRequests) > 0
+ c.Assert(proxyUsed, Equals, true)
+}
+
+func (s *ProxyEnvSuite) prepareRepository(c *C, f *fixtures.Fixture, name string) *transport.Endpoint {
+ fs := f.DotGit()
+
+ err := fixtures.EnsureIsBare(fs)
+ c.Assert(err, IsNil)
+
+ path := filepath.Join(s.base, name)
+ err = os.Rename(fs.Root(), path)
+ c.Assert(err, IsNil)
+
+ return s.newEndpoint(c, name)
+}
+
+func (s *ProxyEnvSuite) newEndpoint(c *C, name string) *transport.Endpoint {
+ ep, err := transport.NewEndpoint(fmt.Sprintf(
+ "ssh://git@localhost:%d/%s/%s", s.port, filepath.ToSlash(s.base), name,
+ ))
+
+ c.Assert(err, IsNil)
+ return ep
+}
+
+type TestProxyRule struct{}
+
+func (dr TestProxyRule) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) {
+ atomic.AddInt32(&socksProxiedRequests, 1)
+ return ctx, true
+}
diff --git a/plumbing/transport/ssh/internal/test/test_utils.go b/plumbing/transport/ssh/internal/test/test_utils.go
new file mode 100644
index 0000000..c3797b1
--- /dev/null
+++ b/plumbing/transport/ssh/internal/test/test_utils.go
@@ -0,0 +1,83 @@
+package test
+
+import (
+ "fmt"
+ "io"
+ "os/exec"
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/gliderlabs/ssh"
+)
+
+func HandlerSSH(s ssh.Session) {
+ cmd, stdin, stderr, stdout, err := buildCommand(s.Command())
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ if err := cmd.Start(); err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ go func() {
+ defer stdin.Close()
+ io.Copy(stdin, s)
+ }()
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ go func() {
+ defer wg.Done()
+ io.Copy(s.Stderr(), stderr)
+ }()
+
+ go func() {
+ defer wg.Done()
+ io.Copy(s, stdout)
+ }()
+
+ wg.Wait()
+
+ if err := cmd.Wait(); err != nil {
+ return
+ }
+
+}
+
+func buildCommand(c []string) (cmd *exec.Cmd, stdin io.WriteCloser, stderr, stdout io.ReadCloser, err error) {
+ if len(c) != 2 {
+ err = fmt.Errorf("invalid command")
+ return
+ }
+
+ // fix for Windows environments
+ var path string
+ if runtime.GOOS == "windows" {
+ path = strings.Replace(c[1], "/C:/", "C:/", 1)
+ } else {
+ path = c[1]
+ }
+
+ cmd = exec.Command(c[0], path)
+ stdout, err = cmd.StdoutPipe()
+ if err != nil {
+ return
+ }
+
+ stdin, err = cmd.StdinPipe()
+ if err != nil {
+ return
+ }
+
+ stderr, err = cmd.StderrPipe()
+ if err != nil {
+ return
+ }
+
+ return
+}
diff --git a/plumbing/transport/ssh/proxy_test.go b/plumbing/transport/ssh/proxy_test.go
index 3caf1ff..2fab851 100644
--- a/plumbing/transport/ssh/proxy_test.go
+++ b/plumbing/transport/ssh/proxy_test.go
@@ -1,36 +1,88 @@
package ssh
import (
+ "context"
"fmt"
+ "io/ioutil"
"log"
"net"
"os"
+ "sync/atomic"
"github.com/armon/go-socks5"
+ "github.com/gliderlabs/ssh"
+ "github.com/go-git/go-git/v5/plumbing/transport"
+ "github.com/go-git/go-git/v5/plumbing/transport/ssh/internal/test"
+
+ fixtures "github.com/go-git/go-git-fixtures/v4"
+ stdssh "golang.org/x/crypto/ssh"
. "gopkg.in/check.v1"
)
type ProxySuite struct {
- UploadPackSuite
+ u UploadPackSuite
+ fixtures.Suite
}
var _ = Suite(&ProxySuite{})
-func (s *ProxySuite) SetUpSuite(c *C) {
- s.UploadPackSuite.SetUpSuite(c)
+var socksProxiedRequests int32
- l, err := net.Listen("tcp", "localhost:0")
+func (s *ProxySuite) TestCommand(c *C) {
+ socksListener, err := net.Listen("tcp", "localhost:0")
c.Assert(err, IsNil)
- server, err := socks5.New(&socks5.Config{})
+ socksServer, err := socks5.New(&socks5.Config{
+ AuthMethods: []socks5.Authenticator{socks5.UserPassAuthenticator{
+ Credentials: socks5.StaticCredentials{
+ "user": "pass",
+ },
+ }},
+ Rules: TestProxyRule{},
+ })
c.Assert(err, IsNil)
+ go func() {
+ socksServer.Serve(socksListener)
+ }()
+ socksProxyAddr := fmt.Sprintf("socks5://localhost:%d", socksListener.Addr().(*net.TCPAddr).Port)
- port := l.Addr().(*net.TCPAddr).Port
-
- err = os.Setenv("ALL_PROXY", fmt.Sprintf("socks5://localhost:%d", port))
+ sshListener, err := net.Listen("tcp", "localhost:0")
c.Assert(err, IsNil)
-
+ sshServer := &ssh.Server{Handler: test.HandlerSSH}
go func() {
- log.Fatal(server.Serve(l))
+ log.Fatal(sshServer.Serve(sshListener))
}()
+
+ s.u.port = sshListener.Addr().(*net.TCPAddr).Port
+ s.u.base, err = ioutil.TempDir(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.u.port))
+ c.Assert(err, IsNil)
+
+ DefaultAuthBuilder = func(user string) (AuthMethod, error) {
+ return &Password{User: user}, nil
+ }
+
+ ep := s.u.prepareRepository(c, fixtures.Basic().One(), "basic.git")
+ c.Assert(err, IsNil)
+ ep.Proxy = transport.ProxyOptions{
+ URL: socksProxyAddr,
+ Username: "user",
+ Password: "pass",
+ }
+
+ runner := runner{
+ config: &stdssh.ClientConfig{
+ HostKeyCallback: stdssh.InsecureIgnoreHostKey(),
+ },
+ }
+ _, err = runner.Command(transport.UploadPackServiceName, ep, nil)
+ c.Assert(err, IsNil)
+ proxyUsed := atomic.LoadInt32(&socksProxiedRequests) > 0
+ c.Assert(proxyUsed, Equals, true)
+}
+
+type TestProxyRule struct{}
+
+func (dr TestProxyRule) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) {
+ atomic.AddInt32(&socksProxiedRequests, 1)
+ return ctx, true
}
diff --git a/plumbing/transport/ssh/upload_pack_test.go b/plumbing/transport/ssh/upload_pack_test.go
index f172fee..fafff48 100644
--- a/plumbing/transport/ssh/upload_pack_test.go
+++ b/plumbing/transport/ssh/upload_pack_test.go
@@ -14,6 +14,7 @@ import (
"sync"
"github.com/go-git/go-git/v5/plumbing/transport"
+ testutils "github.com/go-git/go-git/v5/plumbing/transport/ssh/internal/test"
"github.com/go-git/go-git/v5/plumbing/transport/test"
"github.com/gliderlabs/ssh"
@@ -57,7 +58,7 @@ func (s *UploadPackSuite) SetUpSuite(c *C) {
s.UploadPackSuite.EmptyEndpoint = s.prepareRepository(c, fixtures.ByTag("empty").One(), "empty.git")
s.UploadPackSuite.NonExistentEndpoint = s.newEndpoint(c, "non-existent.git")
- server := &ssh.Server{Handler: handlerSSH}
+ server := &ssh.Server{Handler: testutils.HandlerSSH}
for _, opt := range s.opts {
opt(server)
}