Add option to configure http/s managed transport #870

Open
darkowlzz wants to merge 2 commits from darkowlzz/http-go-transport-fix-2 into main
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+"/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(