Support redirects for libgit2 managed transport

For backwards compatibility, support for HTTP redirection is enabled when targeting
the same host, and no TLS downgrade took place.

Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
This commit is contained in:
Paulo Gomes 2022-03-15 23:13:15 +00:00 committed by Sunny
parent 43661dd15e
commit 115040e9ea
No known key found for this signature in database
GPG Key ID: 9F3D25DDFF7FA3CF
2 changed files with 101 additions and 30 deletions

View File

@ -44,12 +44,12 @@ THE SOFTWARE.
package managed package managed
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -133,6 +133,25 @@ func (t *httpSmartSubtransport) Action(targetUrl string, action git2go.SmartServ
stream.sendRequestBackground() stream.sendRequestBackground()
} }
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return fmt.Errorf("too many redirects")
}
// golang will change POST to GET in case of redirects.
if len(via) >= 0 && req.Method != via[0].Method {
if via[0].URL.Scheme == "https" && req.URL.Scheme == "http" {
return fmt.Errorf("downgrade from https to http is not allowed: from %q to %q", via[0].URL.String(), req.URL.String())
}
if via[0].URL.Host != req.URL.Host {
return fmt.Errorf("cross hosts redirects are not allowed: from %s to %s", via[0].URL.Host, req.URL.Host)
}
return http.ErrUseLastResponse
}
return nil
}
return stream, nil return stream, nil
} }
@ -165,7 +184,10 @@ func createClientRequest(targetUrl string, action git2go.SmartServiceAction, t *
} }
} }
client := &http.Client{Transport: t, Timeout: fullHttpClientTimeOut} client := &http.Client{
Transport: t,
Timeout: fullHttpClientTimeOut,
}
switch action { switch action {
case git2go.SmartServiceActionUploadpackLs: case git2go.SmartServiceActionUploadpackLs:
@ -218,6 +240,7 @@ type httpSmartSubtransportStream struct {
recvReply sync.WaitGroup recvReply sync.WaitGroup
httpError error httpError error
m sync.RWMutex m sync.RWMutex
targetURL string
} }
func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream { func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream {
@ -246,18 +269,21 @@ func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) {
self.recvReply.Wait() self.recvReply.Wait()
self.m.RLock() self.m.RLock()
defer self.m.RUnlock() err := self.httpError
if self.httpError != nil { self.m.RUnlock()
if err != nil {
return 0, self.httpError return 0, self.httpError
} }
return self.resp.Body.Read(buf) return self.resp.Body.Read(buf)
} }
func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) { func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) {
self.m.RLock() self.m.RLock()
defer self.m.RUnlock() err := self.httpError
if self.httpError != nil { self.m.RUnlock()
if err != nil {
return 0, self.httpError return 0, self.httpError
} }
return self.writer.Write(buf) return self.writer.Write(buf)
@ -308,29 +334,53 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
} }
} }
req := &http.Request{ var content []byte
Method: self.req.Method, for {
URL: self.req.URL, req := &http.Request{
Header: self.req.Header, Method: self.req.Method,
} URL: self.req.URL,
if req.Method == "POST" { Header: self.req.Header,
req.Body = self.reader }
req.ContentLength = -1 if req.Method == "POST" {
if len(content) == 0 {
// a copy of the request body needs to be saved so
// it can be reused in case of redirects.
if content, err = io.ReadAll(self.reader); err != nil {
return err
}
}
req.Body = io.NopCloser(bytes.NewReader(content))
req.ContentLength = -1
}
req.SetBasicAuth(userName, password)
resp, err = self.client.Do(req)
if err != nil {
return err
}
// GET requests will be automatically redirected.
// POST require the new destination, and also the body content.
if req.Method == "POST" && resp.StatusCode >= 301 && resp.StatusCode <= 308 {
// The next try will go against the new destination
self.req.URL, err = resp.Location()
if err != nil {
return err
}
continue
}
if resp.StatusCode == http.StatusOK {
break
}
io.Copy(io.Discard, resp.Body)
defer resp.Body.Close()
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
} }
req.SetBasicAuth(userName, password) self.resp = resp
resp, err = self.client.Do(req) self.sentRequest = true
if err != nil { return nil
return err
}
if resp.StatusCode == http.StatusOK {
self.resp = resp
self.sentRequest = true
return nil
}
io.Copy(ioutil.Discard, resp.Body)
defer resp.Body.Close()
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
} }

View File

@ -304,3 +304,24 @@ func TestManagedTransport_E2E(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
repo.Free() repo.Free()
} }
func TestManagedTransport_HandleRedirect(t *testing.T) {
g := NewWithT(t)
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
// Force managed transport to be enabled
InitManagedTransport()
// GitHub will cause a 301 and redirect to https
repo, err := git2go.Clone("http://github.com/stefanprodan/podinfo", tmpDir, &git2go.CloneOptions{
FetchOptions: git2go.FetchOptions{},
CheckoutOptions: git2go.CheckoutOptions{
Strategy: git2go.CheckoutForce,
},
})
g.Expect(err).ToNot(HaveOccurred())
repo.Free()
}