Update HTTPFallback to handle tls handshake timeout
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
parent
7c50784591
commit
5e470e1cae
@ -244,7 +244,7 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
|
|||||||
|
|
||||||
// When TLS has been configured for the operation or host and
|
// When TLS has been configured for the operation or host and
|
||||||
// the protocol from the port number is ambiguous, use the
|
// the protocol from the port number is ambiguous, use the
|
||||||
// docker.HTTPFallback roundtripper to catch TLS errors and re-attempt the
|
// docker.NewHTTPFallback roundtripper to catch TLS errors and re-attempt the
|
||||||
// request as http. This allows preference for https when configured but
|
// request as http. This allows preference for https when configured but
|
||||||
// also catches TLS errors early enough in the request to avoid sending
|
// also catches TLS errors early enough in the request to avoid sending
|
||||||
// the request twice or consuming the request body.
|
// the request twice or consuming the request body.
|
||||||
@ -253,7 +253,7 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
|
|||||||
if port != "" && port != "80" {
|
if port != "" && port != "80" {
|
||||||
log.G(ctx).WithField("host", host.host).Info("host will try HTTPS first since it is configured for HTTP with a TLS configuration, consider changing host to HTTPS or removing unused TLS configuration")
|
log.G(ctx).WithField("host", host.host).Info("host will try HTTPS first since it is configured for HTTP with a TLS configuration, consider changing host to HTTPS or removing unused TLS configuration")
|
||||||
host.scheme = "https"
|
host.scheme = "https"
|
||||||
rhosts[i].Client.Transport = docker.HTTPFallback{RoundTripper: rhosts[i].Client.Transport}
|
rhosts[i].Client.Transport = docker.NewHTTPFallback(rhosts[i].Client.Transport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,11 +472,11 @@ func TestHTTPFallback(t *testing.T) {
|
|||||||
if testHosts[0].Scheme != tc.expectedScheme {
|
if testHosts[0].Scheme != tc.expectedScheme {
|
||||||
t.Fatalf("expected %s scheme for localhost with tls config, got %q", tc.expectedScheme, testHosts[0].Scheme)
|
t.Fatalf("expected %s scheme for localhost with tls config, got %q", tc.expectedScheme, testHosts[0].Scheme)
|
||||||
}
|
}
|
||||||
_, ok := testHosts[0].Client.Transport.(docker.HTTPFallback)
|
_, defaultTransport := testHosts[0].Client.Transport.(*http.Transport)
|
||||||
if tc.usesFallback && !ok {
|
if tc.usesFallback && defaultTransport {
|
||||||
t.Fatal("expected http fallback configured for defaulted localhost endpoint")
|
t.Fatal("expected http fallback configured for defaulted localhost endpoint")
|
||||||
} else if ok && !tc.usesFallback {
|
} else if !defaultTransport && !tc.usesFallback {
|
||||||
t.Fatal("expected no http fallback configured for defaulted localhost endpoint")
|
t.Fatalf("expected no http fallback configured for defaulted localhost endpoint")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func TestPusherHTTPFallback(t *testing.T) {
|
|||||||
if client.Transport == nil {
|
if client.Transport == nil {
|
||||||
client.Transport = http.DefaultTransport
|
client.Transport = http.DefaultTransport
|
||||||
}
|
}
|
||||||
client.Transport = HTTPFallback{client.Transport}
|
client.Transport = NewHTTPFallback(client.Transport)
|
||||||
p.hosts[0].Client = client
|
p.hosts[0].Client = client
|
||||||
phost := p.hosts[0].Host
|
phost := p.hosts[0].Host
|
||||||
p.hosts[0].Authorizer = NewDockerAuthorizer(WithAuthCreds(func(host string) (string, string, error) {
|
p.hosts[0].Authorizer = NewDockerAuthorizer(WithAuthCreds(func(host string) (string, string, error) {
|
||||||
|
@ -714,24 +714,39 @@ func IsLocalhost(host string) bool {
|
|||||||
return ip.IsLoopback()
|
return ip.IsLoopback()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPFallback is an http.RoundTripper which allows fallback from https to http
|
// NewHTTPFallback returns http.RoundTripper which allows fallback from https to
|
||||||
// for registry endpoints with configurations for both http and TLS, such as
|
// http for registry endpoints with configurations for both http and TLS,
|
||||||
// defaulted localhost endpoints.
|
// such as defaulted localhost endpoints.
|
||||||
type HTTPFallback struct {
|
func NewHTTPFallback(transport http.RoundTripper) http.RoundTripper {
|
||||||
http.RoundTripper
|
return &httpFallback{
|
||||||
|
super: transport,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f HTTPFallback) RoundTrip(r *http.Request) (*http.Response, error) {
|
type httpFallback struct {
|
||||||
resp, err := f.RoundTripper.RoundTrip(r)
|
super http.RoundTripper
|
||||||
var tlsErr tls.RecordHeaderError
|
host string
|
||||||
if errors.As(err, &tlsErr) && string(tlsErr.RecordHeader[:]) == "HTTP/" {
|
}
|
||||||
// server gave HTTP response to HTTPS client
|
|
||||||
plainHTTPUrl := *r.URL
|
|
||||||
plainHTTPUrl.Scheme = "http"
|
|
||||||
|
|
||||||
plainHTTPRequest := *r
|
func (f *httpFallback) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
plainHTTPRequest.URL = &plainHTTPUrl
|
// only fall back if the same host had previously fell back
|
||||||
|
if f.host != r.URL.Host {
|
||||||
|
resp, err := f.super.RoundTrip(r)
|
||||||
|
if !isTLSError(err) {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plainHTTPUrl := *r.URL
|
||||||
|
plainHTTPUrl.Scheme = "http"
|
||||||
|
|
||||||
|
plainHTTPRequest := *r
|
||||||
|
plainHTTPRequest.URL = &plainHTTPUrl
|
||||||
|
|
||||||
|
if f.host != r.URL.Host {
|
||||||
|
f.host = r.URL.Host
|
||||||
|
|
||||||
|
// update body on the second attempt
|
||||||
if r.Body != nil && r.GetBody != nil {
|
if r.Body != nil && r.GetBody != nil {
|
||||||
body, err := r.GetBody()
|
body, err := r.GetBody()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -739,9 +754,22 @@ func (f HTTPFallback) RoundTrip(r *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
plainHTTPRequest.Body = body
|
plainHTTPRequest.Body = body
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.RoundTripper.RoundTrip(&plainHTTPRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return f.super.RoundTrip(&plainHTTPRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTLSError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var tlsErr tls.RecordHeaderError
|
||||||
|
if errors.As(err, &tlsErr) && string(tlsErr.RecordHeader[:]) == "HTTP/" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "TLS handshake timeout") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -406,7 +407,7 @@ func TestHTTPFallbackResolver(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: HTTPFallback{http.DefaultTransport},
|
Transport: NewHTTPFallback(http.DefaultTransport),
|
||||||
}
|
}
|
||||||
options := ResolverOptions{
|
options := ResolverOptions{
|
||||||
Hosts: func(host string) ([]RegistryHost, error) {
|
Hosts: func(host string) ([]RegistryHost, error) {
|
||||||
@ -427,6 +428,59 @@ func TestHTTPFallbackResolver(t *testing.T) {
|
|||||||
runBasicTest(t, "testname", sf)
|
runBasicTest(t, "testname", sf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPFallbackTimeoutResolver(t *testing.T) {
|
||||||
|
sf := func(h http.Handler) (string, ResolverOptions, func()) {
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: h,
|
||||||
|
ReadHeaderTimeout: time.Second,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// Accept first connection but do not do anything with it
|
||||||
|
// to force TLS handshake to timeout. Subsequent connection
|
||||||
|
// will be HTTP and should work.
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
c.Close()
|
||||||
|
}()
|
||||||
|
server.Serve(l)
|
||||||
|
}()
|
||||||
|
host := l.Addr().String()
|
||||||
|
|
||||||
|
defaultTransport := &http.Transport{
|
||||||
|
TLSHandshakeTimeout: time.Millisecond,
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewHTTPFallback(defaultTransport),
|
||||||
|
}
|
||||||
|
|
||||||
|
options := ResolverOptions{
|
||||||
|
Hosts: func(host string) ([]RegistryHost, error) {
|
||||||
|
return []RegistryHost{
|
||||||
|
{
|
||||||
|
Client: client,
|
||||||
|
Host: host,
|
||||||
|
Scheme: "https",
|
||||||
|
Path: "/v2",
|
||||||
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return host, options, func() { l.Close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
runBasicTest(t, "testname", sf)
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveProxy(t *testing.T) {
|
func TestResolveProxy(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
Loading…
Reference in New Issue
Block a user