Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to configure http/s managed transport #870

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 69 additions & 46 deletions http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package git

import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
}

return &httpSmartSubtransport{
transport: transport,
client: &http.Client{
Transport: &http.Transport{
Proxy: proxyFn,
},
},
}, nil
// 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
}
}

type httpSmartSubtransport struct {
Expand All @@ -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+"/git-receive-pack", nil)
if err != nil {
break
}
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down