aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod2
-rw-r--r--go.sum7
-rw-r--r--options.go11
-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
-rw-r--r--remote.go17
-rw-r--r--repository.go1
22 files changed, 883 insertions, 68 deletions
diff --git a/go.mod b/go.mod
index 27b74a3..f062b1a 100644
--- a/go.mod
+++ b/go.mod
@@ -6,11 +6,13 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20230417170513-8ee5748c52b5
github.com/acomagu/bufpipe v1.0.4
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
+ github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819
github.com/emirpasic/gods v1.18.1
github.com/gliderlabs/ssh v0.3.5
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/google/go-cmp v0.5.9
github.com/imdario/mergo v0.3.15
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
diff --git a/go.sum b/go.sum
index f4fb146..db7d92b 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
@@ -26,6 +30,8 @@ github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
@@ -53,6 +59,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
diff --git a/options.go b/options.go
index 04c83de..84744c7 100644
--- a/options.go
+++ b/options.go
@@ -73,6 +73,8 @@ type CloneOptions struct {
InsecureSkipTLS bool
// CABundle specify additional ca bundle with system cert pool
CABundle []byte
+ // ProxyOptions provides info required for connecting to a proxy.
+ ProxyOptions transport.ProxyOptions
}
// Validate validates the fields and sets the default values.
@@ -124,6 +126,8 @@ type PullOptions struct {
InsecureSkipTLS bool
// CABundle specify additional ca bundle with system cert pool
CABundle []byte
+ // ProxyOptions provides info required for connecting to a proxy.
+ ProxyOptions transport.ProxyOptions
}
// Validate validates the fields and sets the default values.
@@ -180,6 +184,8 @@ type FetchOptions struct {
InsecureSkipTLS bool
// CABundle specify additional ca bundle with system cert pool
CABundle []byte
+ // ProxyOptions provides info required for connecting to a proxy.
+ ProxyOptions transport.ProxyOptions
}
// Validate validates the fields and sets the default values.
@@ -243,6 +249,8 @@ type PushOptions struct {
Options map[string]string
// Atomic sets option to be an atomic push
Atomic bool
+ // ProxyOptions provides info required for connecting to a proxy.
+ ProxyOptions transport.ProxyOptions
}
// ForceWithLease sets fields on the lease
@@ -624,10 +632,11 @@ type ListOptions struct {
InsecureSkipTLS bool
// CABundle specify additional ca bundle with system cert pool
CABundle []byte
-
// PeelingOption defines how peeled objects are handled during a
// remote list.
PeelingOption PeelingOption
+ // ProxyOptions provides info required for connecting to a proxy.
+ ProxyOptions transport.ProxyOptions
}
// PeelingOption represents the different ways to handle peeled references.
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)
}
diff --git a/remote.go b/remote.go
index ff72bdf..df26491 100644
--- a/remote.go
+++ b/remote.go
@@ -113,7 +113,7 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
o.RemoteURL = r.c.URLs[0]
}
- s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle)
+ s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions)
if err != nil {
return err
}
@@ -414,7 +414,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
o.RemoteURL = r.c.URLs[0]
}
- s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle)
+ s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions)
if err != nil {
return nil, err
}
@@ -511,8 +511,8 @@ func depthChanged(before []plumbing.Hash, s storage.Storer) (bool, error) {
return false, nil
}
-func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte) (transport.UploadPackSession, error) {
- c, ep, err := newClient(url, auth, insecure, cabundle)
+func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.UploadPackSession, error) {
+ c, ep, err := newClient(url, insecure, cabundle, proxyOpts)
if err != nil {
return nil, err
}
@@ -520,8 +520,8 @@ func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool,
return c.NewUploadPackSession(ep, auth)
}
-func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte) (transport.ReceivePackSession, error) {
- c, ep, err := newClient(url, auth, insecure, cabundle)
+func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.ReceivePackSession, error) {
+ c, ep, err := newClient(url, insecure, cabundle, proxyOpts)
if err != nil {
return nil, err
}
@@ -529,13 +529,14 @@ func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, ca
return c.NewReceivePackSession(ep, auth)
}
-func newClient(url string, auth transport.AuthMethod, insecure bool, cabundle []byte) (transport.Transport, *transport.Endpoint, error) {
+func newClient(url string, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.Transport, *transport.Endpoint, error) {
ep, err := transport.NewEndpoint(url)
if err != nil {
return nil, nil, err
}
ep.InsecureSkipTLS = insecure
ep.CaBundle = cabundle
+ ep.Proxy = proxyOpts
c, err := client.NewClient(ep)
if err != nil {
@@ -1267,7 +1268,7 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe
return nil, ErrEmptyUrls
}
- s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle)
+ s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions)
if err != nil {
return nil, err
}
diff --git a/repository.go b/repository.go
index be5f140..287b597 100644
--- a/repository.go
+++ b/repository.go
@@ -873,6 +873,7 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error {
RemoteName: o.RemoteName,
InsecureSkipTLS: o.InsecureSkipTLS,
CABundle: o.CABundle,
+ ProxyOptions: o.ProxyOptions,
}, o.ReferenceName)
if err != nil {
return err