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
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,7 +38,14 @@ func registerManagedHTTP() error {
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)
proxyOpts, err := transport.SmartProxyOptions()
if err != nil {
@ -56,14 +65,26 @@ func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSu
proxyFn = http.ProxyURL(parsedUrl)
}
return &httpSmartSubtransport{
transport: transport,
client: &http.Client{
Transport: &http.Transport{
// Add the proxy to the http transport.
httpTransport := &http.Transport{
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 {
@ -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)

View File

@ -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(