From 3d4b9b97d15e4d0d7366e2382a9c17450275cc65 Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 22 Nov 2021 19:00:14 +0530 Subject: [PATCH] Add option to configure http/s managed transport This change introduces NewRegisterSmartTransportWithOptions() to help configure the smart transport with SmartSubtransportOptions. If the default smart subtransport client needs to be configured, a newly configured smart transport can be registered and used. The SmartSubtransportOptions includes CABundle only for now. This enables creating and using new transport with secrets that can be deleted and not shared with subsequent operations. The http client from httpSmartSubtransport is now shared with the underlying httpSmartSubtransportStream, reusing the client and its configurations. It also fixes the error during cloning: ``` unable to clone: Post "http://test-user:***@127.0.0.1:40463/bar/test-reponame/git-upload-pack": io: read/write on closed pipe ``` by using credentials if available and avoiding failure due to unauthorized request. A user of the smart transport who needs to add a CA bundle in the http client can do the following to setup the smart transport before cloning: ``` stOpts := &git2go.SmartSubtransportOptions{CABundle: opts.CAFile} rst, err := git2go.NewRegisterSmartTransportWithOptions("https", stOpts) if err != nil { return err } if rst != nil { defer rst.Free() } ``` --- http.go | 115 ++++++++++++++++++++++++++++++--------------------- transport.go | 16 +++++++ 2 files changed, 85 insertions(+), 46 deletions(-) diff --git a/http.go b/http.go index 0777c56..830e467 100644 --- a/http.go +++ b/http.go @@ -1,6 +1,8 @@ package git import ( + "crypto/tls" + "crypto/x509" "errors" "fmt" "io" @@ -16,7 +18,7 @@ import ( // If Shutdown or ReInit are called, make sure that the smart transports are // freed before it. func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) { - return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory) + return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory(nil)) } func registerManagedHTTP() error { @@ -27,7 +29,7 @@ func registerManagedHTTP() error { if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok { continue } - managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true) + managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory(nil), true) if err != nil { return fmt.Errorf("failed to register transport for %q: %v", protocol, err) } @@ -36,34 +38,53 @@ func registerManagedHTTP() error { return nil } -func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) { - var proxyFn func(*http.Request) (*url.URL, error) - proxyOpts, err := transport.SmartProxyOptions() - if err != nil { - return nil, err - } - switch proxyOpts.Type { - case ProxyTypeNone: - proxyFn = nil - case ProxyTypeAuto: - proxyFn = http.ProxyFromEnvironment - case ProxyTypeSpecified: - parsedUrl, err := url.Parse(proxyOpts.Url) +// httpSmartSubtransportFactory implements SmartSubtransportCallback which +// returns a SmartSubtransport for a remote and transport. +func httpSmartSubtransportFactory(opts *SmartSubtransportOptions) SmartSubtransportCallback { + return func(remote *Remote, transport *Transport) (SmartSubtransport, error) { + sst := &httpSmartSubtransport{ + transport: transport, + } + + var proxyFn func(*http.Request) (*url.URL, error) + proxyOpts, err := transport.SmartProxyOptions() if err != nil { return nil, err } + switch proxyOpts.Type { + case ProxyTypeNone: + proxyFn = nil + case ProxyTypeAuto: + proxyFn = http.ProxyFromEnvironment + case ProxyTypeSpecified: + parsedUrl, err := url.Parse(proxyOpts.Url) + if err != nil { + return nil, err + } - proxyFn = http.ProxyURL(parsedUrl) + proxyFn = http.ProxyURL(parsedUrl) + } + + // Add the proxy to the http transport. + httpTransport := &http.Transport{ + Proxy: proxyFn, + } + + // Add any provided certificate to the http transport. + if opts != nil && len(opts.CABundle) > 0 { + cap := x509.NewCertPool() + if ok := cap.AppendCertsFromPEM(opts.CABundle); !ok { + return nil, fmt.Errorf("failed to use certificate from PEM") + } + httpTransport.TLSClientConfig = &tls.Config{ + RootCAs: cap, + } + } + + sst.client = &http.Client{Transport: httpTransport} + + return sst, nil } - - return &httpSmartSubtransport{ - transport: transport, - client: &http.Client{ - Transport: &http.Transport{ - Proxy: proxyFn, - }, - }, - }, nil } type httpSmartSubtransport struct { @@ -89,7 +110,7 @@ func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (S req, err = http.NewRequest("GET", url+"/info/refs?service=git-receive-pack", nil) case SmartServiceActionReceivepack: - req, err = http.NewRequest("POST", url+"/info/refs?service=git-upload-pack", nil) + req, err = http.NewRequest("POST", url+"/info/refs?service=git-receive-pack", nil) if err != nil { break } @@ -105,7 +126,7 @@ func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (S req.Header.Set("User-Agent", "git/2.0 (git2go)") - stream := newManagedHttpStream(t, req) + stream := newManagedHttpStream(t, req, t.client) if req.Method == "POST" { stream.recvReply.Add(1) stream.sendRequestBackground() @@ -124,6 +145,7 @@ func (t *httpSmartSubtransport) Free() { type httpSmartSubtransportStream struct { owner *httpSmartSubtransport + client *http.Client req *http.Request resp *http.Response reader *io.PipeReader @@ -133,10 +155,11 @@ type httpSmartSubtransportStream struct { httpError error } -func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request) *httpSmartSubtransportStream { +func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream { r, w := io.Pipe() return &httpSmartSubtransportStream{ owner: owner, + client: client, req: req, reader: r, writer: w, @@ -192,6 +215,23 @@ func (self *httpSmartSubtransportStream) sendRequest() error { var err error var userName string var password string + + // Obtain the credentials and use them if available. + cred, err := self.owner.transport.SmartCredentials("", CredentialTypeUserpassPlaintext) + if err != nil { + // Passthrough error indicates that no credentials were provided. + // Continue without credentials. + if err.Error() != ErrorCodePassthrough.String() { + return err + } + } else { + userName, password, err = cred.GetUserpassPlaintext() + if err != nil { + return err + } + defer cred.Free() + } + for { req := &http.Request{ Method: self.req.Method, @@ -204,7 +244,7 @@ func (self *httpSmartSubtransportStream) sendRequest() error { } req.SetBasicAuth(userName, password) - resp, err = http.DefaultClient.Do(req) + resp, err = self.client.Do(req) if err != nil { return err } @@ -213,23 +253,6 @@ func (self *httpSmartSubtransportStream) sendRequest() error { break } - if resp.StatusCode == http.StatusUnauthorized { - resp.Body.Close() - - cred, err := self.owner.transport.SmartCredentials("", CredentialTypeUserpassPlaintext) - if err != nil { - return err - } - defer cred.Free() - - userName, password, err = cred.GetUserpassPlaintext() - if err != nil { - return err - } - - continue - } - // Any other error we treat as a hard error and punt back to the caller resp.Body.Close() return fmt.Errorf("Unhandled HTTP error %s", resp.Status) diff --git a/transport.go b/transport.go index 23514b4..1073fcc 100644 --- a/transport.go +++ b/transport.go @@ -219,6 +219,22 @@ type RegisteredSmartTransport struct { handle unsafe.Pointer } +// SmartSubtransportOptions is an option for configuring the smart subtransport. +type SmartSubtransportOptions struct { + CABundle []byte +} + +// NewRegisterSmartTransportWithOptions registers Go-native transport configured +// with options. +func NewRegisterSmartTransportWithOptions(protocol string, opts *SmartSubtransportOptions) (*RegisteredSmartTransport, error) { + switch protocol { + case "http", "https": + return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory(opts)) + default: + return nil, nil + } +} + // NewRegisteredSmartTransport adds a custom transport definition, to be used // in addition to the built-in set of transports that come with libgit2. func NewRegisteredSmartTransport(