Skip to content

Commit 115040e

Browse files
Paulo Gomesdarkowlzz
authored andcommitted
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 <[email protected]>
1 parent 43661dd commit 115040e

File tree

2 files changed

+99
-28
lines changed

2 files changed

+99
-28
lines changed

pkg/git/libgit2/managed/http.go

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ THE SOFTWARE.
4444
package managed
4545

4646
import (
47+
"bytes"
4748
"crypto/tls"
4849
"crypto/x509"
4950
"errors"
5051
"fmt"
5152
"io"
52-
"io/ioutil"
5353
"net"
5454
"net/http"
5555
"net/url"
@@ -133,6 +133,25 @@ func (t *httpSmartSubtransport) Action(targetUrl string, action git2go.SmartServ
133133
stream.sendRequestBackground()
134134
}
135135

136+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
137+
if len(via) >= 3 {
138+
return fmt.Errorf("too many redirects")
139+
}
140+
141+
// golang will change POST to GET in case of redirects.
142+
if len(via) >= 0 && req.Method != via[0].Method {
143+
if via[0].URL.Scheme == "https" && req.URL.Scheme == "http" {
144+
return fmt.Errorf("downgrade from https to http is not allowed: from %q to %q", via[0].URL.String(), req.URL.String())
145+
}
146+
if via[0].URL.Host != req.URL.Host {
147+
return fmt.Errorf("cross hosts redirects are not allowed: from %s to %s", via[0].URL.Host, req.URL.Host)
148+
}
149+
150+
return http.ErrUseLastResponse
151+
}
152+
return nil
153+
}
154+
136155
return stream, nil
137156
}
138157

@@ -165,7 +184,10 @@ func createClientRequest(targetUrl string, action git2go.SmartServiceAction, t *
165184
}
166185
}
167186

168-
client := &http.Client{Transport: t, Timeout: fullHttpClientTimeOut}
187+
client := &http.Client{
188+
Transport: t,
189+
Timeout: fullHttpClientTimeOut,
190+
}
169191

170192
switch action {
171193
case git2go.SmartServiceActionUploadpackLs:
@@ -218,6 +240,7 @@ type httpSmartSubtransportStream struct {
218240
recvReply sync.WaitGroup
219241
httpError error
220242
m sync.RWMutex
243+
targetURL string
221244
}
222245

223246
func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream {
@@ -246,18 +269,21 @@ func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) {
246269
self.recvReply.Wait()
247270

248271
self.m.RLock()
249-
defer self.m.RUnlock()
250-
if self.httpError != nil {
272+
err := self.httpError
273+
self.m.RUnlock()
274+
275+
if err != nil {
251276
return 0, self.httpError
252277
}
253-
254278
return self.resp.Body.Read(buf)
255279
}
256280

257281
func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) {
258282
self.m.RLock()
259-
defer self.m.RUnlock()
260-
if self.httpError != nil {
283+
err := self.httpError
284+
self.m.RUnlock()
285+
286+
if err != nil {
261287
return 0, self.httpError
262288
}
263289
return self.writer.Write(buf)
@@ -308,29 +334,53 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
308334
}
309335
}
310336

311-
req := &http.Request{
312-
Method: self.req.Method,
313-
URL: self.req.URL,
314-
Header: self.req.Header,
315-
}
316-
if req.Method == "POST" {
317-
req.Body = self.reader
318-
req.ContentLength = -1
319-
}
337+
var content []byte
338+
for {
339+
req := &http.Request{
340+
Method: self.req.Method,
341+
URL: self.req.URL,
342+
Header: self.req.Header,
343+
}
344+
if req.Method == "POST" {
345+
if len(content) == 0 {
346+
// a copy of the request body needs to be saved so
347+
// it can be reused in case of redirects.
348+
if content, err = io.ReadAll(self.reader); err != nil {
349+
return err
350+
}
351+
}
352+
req.Body = io.NopCloser(bytes.NewReader(content))
353+
req.ContentLength = -1
354+
}
320355

321-
req.SetBasicAuth(userName, password)
322-
resp, err = self.client.Do(req)
323-
if err != nil {
324-
return err
325-
}
356+
req.SetBasicAuth(userName, password)
357+
resp, err = self.client.Do(req)
358+
if err != nil {
359+
return err
360+
}
326361

327-
if resp.StatusCode == http.StatusOK {
328-
self.resp = resp
329-
self.sentRequest = true
330-
return nil
362+
// GET requests will be automatically redirected.
363+
// POST require the new destination, and also the body content.
364+
if req.Method == "POST" && resp.StatusCode >= 301 && resp.StatusCode <= 308 {
365+
// The next try will go against the new destination
366+
self.req.URL, err = resp.Location()
367+
if err != nil {
368+
return err
369+
}
370+
371+
continue
372+
}
373+
374+
if resp.StatusCode == http.StatusOK {
375+
break
376+
}
377+
378+
io.Copy(io.Discard, resp.Body)
379+
defer resp.Body.Close()
380+
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
331381
}
332382

333-
io.Copy(ioutil.Discard, resp.Body)
334-
defer resp.Body.Close()
335-
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
383+
self.resp = resp
384+
self.sentRequest = true
385+
return nil
336386
}

pkg/git/libgit2/managed/managed_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,24 @@ func TestManagedTransport_E2E(t *testing.T) {
304304
g.Expect(err).ToNot(HaveOccurred())
305305
repo.Free()
306306
}
307+
308+
func TestManagedTransport_HandleRedirect(t *testing.T) {
309+
g := NewWithT(t)
310+
311+
tmpDir, _ := os.MkdirTemp("", "test")
312+
defer os.RemoveAll(tmpDir)
313+
314+
// Force managed transport to be enabled
315+
InitManagedTransport()
316+
317+
// GitHub will cause a 301 and redirect to https
318+
repo, err := git2go.Clone("http://github.com/stefanprodan/podinfo", tmpDir, &git2go.CloneOptions{
319+
FetchOptions: git2go.FetchOptions{},
320+
CheckoutOptions: git2go.CheckoutOptions{
321+
Strategy: git2go.CheckoutForce,
322+
},
323+
})
324+
325+
g.Expect(err).ToNot(HaveOccurred())
326+
repo.Free()
327+
}

0 commit comments

Comments
 (0)