package git import ( "errors" "fmt" "io" "net/http" "net/url" "sync" ) // RegisterManagedHTTPTransport registers a Go-native implementation of an // HTTP/S transport that doesn't rely on any system libraries (e.g. // libopenssl/libmbedtls). // // 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) } func registerManagedHTTP() error { globalRegisteredSmartTransports.Lock() defer globalRegisteredSmartTransports.Unlock() for _, protocol := range []string{"http", "https"} { if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok { continue } managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true) if err != nil { return fmt.Errorf("failed to register transport for %q: %v", protocol, err) } globalRegisteredSmartTransports.transports[protocol] = managed } return nil } func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) { var proxyFn func(*http.Request) (*url.URL, error) remoteConnectOpts, err := transport.SmartRemoteConnectOptions() if err != nil { return nil, err } switch remoteConnectOpts.ProxyOptions.Type { case ProxyTypeNone: proxyFn = nil case ProxyTypeAuto: proxyFn = http.ProxyFromEnvironment case ProxyTypeSpecified: parsedUrl, err := url.Parse(remoteConnectOpts.ProxyOptions.Url) if err != nil { return nil, err } proxyFn = http.ProxyURL(parsedUrl) } return &httpSmartSubtransport{ transport: transport, client: &http.Client{ Transport: &http.Transport{ Proxy: proxyFn, }, }, }, nil } type httpSmartSubtransport struct { transport *Transport client *http.Client } func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (SmartSubtransportStream, error) { var req *http.Request var err error switch action { case SmartServiceActionUploadpackLs: req, err = http.NewRequest("GET", url+"/info/refs?service=git-upload-pack", nil) case SmartServiceActionUploadpack: req, err = http.NewRequest("POST", url+"/git-upload-pack", nil) if err != nil { break } req.Header.Set("Content-Type", "application/x-git-upload-pack-request") case SmartServiceActionReceivepackLs: 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) if err != nil { break } req.Header.Set("Content-Type", "application/x-git-receive-pack-request") default: err = errors.New("unknown action") } if err != nil { return nil, err } req.Header.Set("User-Agent", "git/2.0 (libgit2)") stream := newManagedHttpStream(t, req) if req.Method == "POST" { stream.recvReply.Add(1) stream.sendRequestBackground() } return stream, nil } func (t *httpSmartSubtransport) Close() error { return nil } func (t *httpSmartSubtransport) Free() { t.client = nil } type httpSmartSubtransportStream struct { owner *httpSmartSubtransport req *http.Request resp *http.Response reader *io.PipeReader writer *io.PipeWriter sentRequest bool recvReply sync.WaitGroup httpError error } func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request) *httpSmartSubtransportStream { r, w := io.Pipe() return &httpSmartSubtransportStream{ owner: owner, req: req, reader: r, writer: w, } } func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) { if !self.sentRequest { self.recvReply.Add(1) if err := self.sendRequest(); err != nil { return 0, err } } if err := self.writer.Close(); err != nil { return 0, err } self.recvReply.Wait() if self.httpError != nil { return 0, self.httpError } return self.resp.Body.Read(buf) } func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) { if self.httpError != nil { return 0, self.httpError } return self.writer.Write(buf) } func (self *httpSmartSubtransportStream) Free() { if self.resp != nil { self.resp.Body.Close() } } func (self *httpSmartSubtransportStream) sendRequestBackground() { go func() { self.httpError = self.sendRequest() }() self.sentRequest = true } func (self *httpSmartSubtransportStream) sendRequest() error { defer self.recvReply.Done() self.resp = nil var resp *http.Response var err error var userName string var password string for { req := &http.Request{ Method: self.req.Method, URL: self.req.URL, Header: self.req.Header, } if req.Method == "POST" { req.Body = self.reader req.ContentLength = -1 } if userName != "" && password != "" { req.SetBasicAuth(userName, password) } resp, err = http.DefaultClient.Do(req) if err != nil { return err } if resp.StatusCode == http.StatusOK { 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) } self.sentRequest = true self.resp = resp return nil }