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()
}
```
This commit is contained in:
Sunny 2021-11-22 19:00:14 +05:30
parent 5e35338d58
commit 3d4b9b97d1
No known key found for this signature in database
GPG Key ID: 9F3D25DDFF7FA3CF
2 changed files with 85 additions and 46 deletions

85
http.go
View File

@ -1,6 +1,8 @@
package git package git
import ( import (
"crypto/tls"
"crypto/x509"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -16,7 +18,7 @@ import (
// If Shutdown or ReInit are called, make sure that the smart transports are // If Shutdown or ReInit are called, make sure that the smart transports are
// freed before it. // freed before it.
func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) { func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) {
return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory) return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory(nil))
} }
func registerManagedHTTP() error { func registerManagedHTTP() error {
@ -27,7 +29,7 @@ func registerManagedHTTP() error {
if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok { if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok {
continue continue
} }
managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true) managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory(nil), true)
if err != nil { if err != nil {
return fmt.Errorf("failed to register transport for %q: %v", protocol, err) return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
} }
@ -36,7 +38,14 @@ func registerManagedHTTP() error {
return nil return nil
} }
func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) { // 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) var proxyFn func(*http.Request) (*url.URL, error)
proxyOpts, err := transport.SmartProxyOptions() proxyOpts, err := transport.SmartProxyOptions()
if err != nil { if err != nil {
@ -56,14 +65,26 @@ func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSu
proxyFn = http.ProxyURL(parsedUrl) proxyFn = http.ProxyURL(parsedUrl)
} }
return &httpSmartSubtransport{ // Add the proxy to the http transport.
transport: transport, httpTransport := &http.Transport{
client: &http.Client{
Transport: &http.Transport{
Proxy: proxyFn, Proxy: proxyFn,
}, }
},
}, nil // 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
}
} }
type httpSmartSubtransport struct { 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) req, err = http.NewRequest("GET", url+"/info/refs?service=git-receive-pack", nil)
case SmartServiceActionReceivepack: 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 { if err != nil {
break break
} }
@ -105,7 +126,7 @@ func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (S
req.Header.Set("User-Agent", "git/2.0 (git2go)") req.Header.Set("User-Agent", "git/2.0 (git2go)")
stream := newManagedHttpStream(t, req) stream := newManagedHttpStream(t, req, t.client)
if req.Method == "POST" { if req.Method == "POST" {
stream.recvReply.Add(1) stream.recvReply.Add(1)
stream.sendRequestBackground() stream.sendRequestBackground()
@ -124,6 +145,7 @@ func (t *httpSmartSubtransport) Free() {
type httpSmartSubtransportStream struct { type httpSmartSubtransportStream struct {
owner *httpSmartSubtransport owner *httpSmartSubtransport
client *http.Client
req *http.Request req *http.Request
resp *http.Response resp *http.Response
reader *io.PipeReader reader *io.PipeReader
@ -133,10 +155,11 @@ type httpSmartSubtransportStream struct {
httpError error 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() r, w := io.Pipe()
return &httpSmartSubtransportStream{ return &httpSmartSubtransportStream{
owner: owner, owner: owner,
client: client,
req: req, req: req,
reader: r, reader: r,
writer: w, writer: w,
@ -192,6 +215,23 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
var err error var err error
var userName string var userName string
var password 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 { for {
req := &http.Request{ req := &http.Request{
Method: self.req.Method, Method: self.req.Method,
@ -204,7 +244,7 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
} }
req.SetBasicAuth(userName, password) req.SetBasicAuth(userName, password)
resp, err = http.DefaultClient.Do(req) resp, err = self.client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -213,23 +253,6 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
break 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 // Any other error we treat as a hard error and punt back to the caller
resp.Body.Close() resp.Body.Close()
return fmt.Errorf("Unhandled HTTP error %s", resp.Status) return fmt.Errorf("Unhandled HTTP error %s", resp.Status)

View File

@ -219,6 +219,22 @@ type RegisteredSmartTransport struct {
handle unsafe.Pointer 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 // NewRegisteredSmartTransport adds a custom transport definition, to be used
// in addition to the built-in set of transports that come with libgit2. // in addition to the built-in set of transports that come with libgit2.
func NewRegisteredSmartTransport( func NewRegisteredSmartTransport(