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.
This commit is contained in:
brian m. carlson 2019-10-28 17:02:28 +00:00
parent 47b03957d8
commit bdacae1fbe
No known key found for this signature in database
GPG Key ID: 2D0C9BC12F82B3A1
2 changed files with 97 additions and 1 deletions

@ -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,

@ -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)
}
}
}