From bdacae1fbeac4082e7e49249b935d1352e970019 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Mon, 28 Oct 2019 17:02:28 +0000 Subject: [PATCH] lfshttp: support http.version There are some servers that cannot speak HTTP/2 in all cases and demand to fall back to HTTP/1.1 with a HTTP_1_1_REQUIRED. Notably, this happens with IIS 10 when using NTLM. Go's HTTP library doesn't seem to like this response and aborts the transfer, leading to a failure. Fortunately, Git has an option (http.version) to control the protocol used when speaking HTTP to a remote server. Implement this option to allow users to set the protocol to use when speaking HTTP and work around these broken servers. --- lfshttp/client.go | 24 +++++++++++++- lfshttp/client_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/lfshttp/client.go b/lfshttp/client.go index 10e5ae59..2240917a 100644 --- a/lfshttp/client.go +++ b/lfshttp/client.go @@ -346,6 +346,26 @@ func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, remote str return c.doWithRedirects(cli, redirectedReq, remote, via) } +func (c *Client) configureProtocols(u *url.URL, tr *http.Transport) error { + version, _ := c.uc.Get("http", u.String(), "version") + switch version { + case "HTTP/1.1": + // This disables HTTP/2, according to the documentation. + tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + case "HTTP/2": + if u.Scheme != "https" { + return fmt.Errorf("HTTP/2 cannot be used except with TLS") + } + http2.ConfigureTransport(tr) + delete(tr.TLSNextProto, "http/1.1") + case "": + http2.ConfigureTransport(tr) + default: + return fmt.Errorf("Unknown HTTP version %q", version) + } + return nil +} + func (c *Client) HttpClient(u *url.URL) (*http.Client, error) { c.clientMu.Lock() defer c.clientMu.Unlock() @@ -443,7 +463,9 @@ func (c *Client) HttpClient(u *url.URL) (*http.Client, error) { tr.TLSClientConfig.RootCAs = getRootCAsForHost(c, host) } - http2.ConfigureTransport(tr) + if err := c.configureProtocols(u, tr); err != nil { + return nil, err + } httpClient := &http.Client{ Transport: tr, diff --git a/lfshttp/client_test.go b/lfshttp/client_test.go index a65a6050..3e8aa5a5 100644 --- a/lfshttp/client_test.go +++ b/lfshttp/client_test.go @@ -335,3 +335,77 @@ func TestHttp2(t *testing.T) { assert.Equal(t, 200, res.StatusCode) assert.EqualValues(t, 1, calledSrv) } + +func TestHttpVersion(t *testing.T) { + testcases := []struct { + Proto string + Setting string + TLSOk bool + PlaintextOk bool + Error string + }{ + {"HTTP/2.0", "HTTP/2", true, false, "HTTP/2 cannot be used except with TLS"}, + {"HTTP/1.1", "HTTP/1.1", true, true, ""}, + {"HTTP/2.0", "lalala", false, false, `Unknown HTTP version "lalala"`}, + } + + for _, test := range testcases { + var calledSrvTLS uint32 + var calledSrv uint32 + + srvTLS := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddUint32(&calledSrvTLS, 1) + assert.Equal(t, "GET", r.Method) + assert.Equal(t, test.Proto, r.Proto) + w.WriteHeader(200) + })) + srvTLS.TLS = &tls.Config{NextProtos: []string{"h2", "http/1.1"}} + srvTLS.StartTLS() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddUint32(&calledSrv, 1) + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "HTTP/1.1", r.Proto) + w.WriteHeader(200) + })) + + defer srvTLS.Close() + defer srv.Close() + + c, err := NewClient(NewContext(nil, nil, map[string]string{ + "http.sslverify": "false", + "http.version": test.Setting, + })) + require.Nil(t, err) + + req, err := http.NewRequest("GET", srvTLS.URL, nil) + require.Nil(t, err) + + if test.TLSOk { + res, err := c.Do(req) + require.Nil(t, err) + assert.Equal(t, 200, res.StatusCode) + assert.EqualValues(t, 1, calledSrvTLS) + } else { + _, err := c.Do(req) + require.NotNil(t, err) + assert.EqualValues(t, err.Error(), test.Error) + assert.EqualValues(t, 0, calledSrv) + } + + req, err = http.NewRequest("GET", srv.URL, nil) + require.Nil(t, err) + + if test.PlaintextOk { + res, err := c.Do(req) + require.Nil(t, err) + assert.Equal(t, 200, res.StatusCode) + assert.EqualValues(t, 1, calledSrv) + } else { + _, err := c.Do(req) + require.NotNil(t, err) + assert.EqualValues(t, err.Error(), test.Error) + assert.EqualValues(t, 0, calledSrv) + } + } +}