github.com/tompao/docker@v1.9.1/registry/endpoint.go (about)

     1  package registry
     2  
     3  import (
     4  	"crypto/tls"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/distribution/registry/api/v2"
    15  	"github.com/docker/distribution/registry/client/transport"
    16  )
    17  
    18  // for mocking in unit tests
    19  var lookupIP = net.LookupIP
    20  
    21  // scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
    22  func scanForAPIVersion(address string) (string, APIVersion) {
    23  	var (
    24  		chunks        []string
    25  		apiVersionStr string
    26  	)
    27  
    28  	if strings.HasSuffix(address, "/") {
    29  		address = address[:len(address)-1]
    30  	}
    31  
    32  	chunks = strings.Split(address, "/")
    33  	apiVersionStr = chunks[len(chunks)-1]
    34  
    35  	for k, v := range apiVersions {
    36  		if apiVersionStr == v {
    37  			address = strings.Join(chunks[:len(chunks)-1], "/")
    38  			return address, k
    39  		}
    40  	}
    41  
    42  	return address, APIVersionUnknown
    43  }
    44  
    45  // NewEndpoint parses the given address to return a registry endpoint.  v can be used to
    46  // specify a specific endpoint version
    47  func NewEndpoint(index *IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
    48  	tlsConfig, err := newTLSConfig(index.Name, index.Secure)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	endpoint, err := newEndpoint(index.GetAuthConfigKey(), tlsConfig, metaHeaders)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	if v != APIVersionUnknown {
    57  		endpoint.Version = v
    58  	}
    59  	if err := validateEndpoint(endpoint); err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return endpoint, nil
    64  }
    65  
    66  func validateEndpoint(endpoint *Endpoint) error {
    67  	logrus.Debugf("pinging registry endpoint %s", endpoint)
    68  
    69  	// Try HTTPS ping to registry
    70  	endpoint.URL.Scheme = "https"
    71  	if _, err := endpoint.Ping(); err != nil {
    72  		if endpoint.IsSecure {
    73  			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
    74  			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
    75  			return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
    76  		}
    77  
    78  		// If registry is insecure and HTTPS failed, fallback to HTTP.
    79  		logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
    80  		endpoint.URL.Scheme = "http"
    81  
    82  		var err2 error
    83  		if _, err2 = endpoint.Ping(); err2 == nil {
    84  			return nil
    85  		}
    86  
    87  		return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
    88  	}
    89  
    90  	return nil
    91  }
    92  
    93  func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) {
    94  	var (
    95  		endpoint       = new(Endpoint)
    96  		trimmedAddress string
    97  		err            error
    98  	)
    99  
   100  	if !strings.HasPrefix(address, "http") {
   101  		address = "https://" + address
   102  	}
   103  
   104  	endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify)
   105  
   106  	trimmedAddress, endpoint.Version = scanForAPIVersion(address)
   107  
   108  	if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// TODO(tiborvass): make sure a ConnectTimeout transport is used
   113  	tr := NewTransport(tlsConfig)
   114  	endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
   115  	return endpoint, nil
   116  }
   117  
   118  // Endpoint stores basic information about a registry endpoint.
   119  type Endpoint struct {
   120  	client         *http.Client
   121  	URL            *url.URL
   122  	Version        APIVersion
   123  	IsSecure       bool
   124  	AuthChallenges []*AuthorizationChallenge
   125  	URLBuilder     *v2.URLBuilder
   126  }
   127  
   128  // Get the formated URL for the root of this registry Endpoint
   129  func (e *Endpoint) String() string {
   130  	return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
   131  }
   132  
   133  // VersionString returns a formatted string of this
   134  // endpoint address using the given API Version.
   135  func (e *Endpoint) VersionString(version APIVersion) string {
   136  	return fmt.Sprintf("%s/v%d/", e.URL, version)
   137  }
   138  
   139  // Path returns a formatted string for the URL
   140  // of this endpoint with the given path appended.
   141  func (e *Endpoint) Path(path string) string {
   142  	return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
   143  }
   144  
   145  // Ping pings the remote endpoint with v2 and v1 pings to determine the API
   146  // version. It returns a PingResult containing the discovered version. The
   147  // PingResult also indicates whether the registry is standalone or not.
   148  func (e *Endpoint) Ping() (PingResult, error) {
   149  	// The ping logic to use is determined by the registry endpoint version.
   150  	switch e.Version {
   151  	case APIVersion1:
   152  		return e.pingV1()
   153  	case APIVersion2:
   154  		return e.pingV2()
   155  	}
   156  
   157  	// APIVersionUnknown
   158  	// We should try v2 first...
   159  	e.Version = APIVersion2
   160  	regInfo, errV2 := e.pingV2()
   161  	if errV2 == nil {
   162  		return regInfo, nil
   163  	}
   164  
   165  	// ... then fallback to v1.
   166  	e.Version = APIVersion1
   167  	regInfo, errV1 := e.pingV1()
   168  	if errV1 == nil {
   169  		return regInfo, nil
   170  	}
   171  
   172  	e.Version = APIVersionUnknown
   173  	return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
   174  }
   175  
   176  func (e *Endpoint) pingV1() (PingResult, error) {
   177  	logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
   178  
   179  	if e.String() == IndexServer {
   180  		// Skip the check, we know this one is valid
   181  		// (and we never want to fallback to http in case of error)
   182  		return PingResult{Standalone: false}, nil
   183  	}
   184  
   185  	req, err := http.NewRequest("GET", e.Path("_ping"), nil)
   186  	if err != nil {
   187  		return PingResult{Standalone: false}, err
   188  	}
   189  
   190  	resp, err := e.client.Do(req)
   191  	if err != nil {
   192  		return PingResult{Standalone: false}, err
   193  	}
   194  
   195  	defer resp.Body.Close()
   196  
   197  	jsonString, err := ioutil.ReadAll(resp.Body)
   198  	if err != nil {
   199  		return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
   200  	}
   201  
   202  	// If the header is absent, we assume true for compatibility with earlier
   203  	// versions of the registry. default to true
   204  	info := PingResult{
   205  		Standalone: true,
   206  	}
   207  	if err := json.Unmarshal(jsonString, &info); err != nil {
   208  		logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
   209  		// don't stop here. Just assume sane defaults
   210  	}
   211  	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
   212  		logrus.Debugf("Registry version header: '%s'", hdr)
   213  		info.Version = hdr
   214  	}
   215  	logrus.Debugf("PingResult.Version: %q", info.Version)
   216  
   217  	standalone := resp.Header.Get("X-Docker-Registry-Standalone")
   218  	logrus.Debugf("Registry standalone header: '%s'", standalone)
   219  	// Accepted values are "true" (case-insensitive) and "1".
   220  	if strings.EqualFold(standalone, "true") || standalone == "1" {
   221  		info.Standalone = true
   222  	} else if len(standalone) > 0 {
   223  		// there is a header set, and it is not "true" or "1", so assume fails
   224  		info.Standalone = false
   225  	}
   226  	logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
   227  	return info, nil
   228  }
   229  
   230  func (e *Endpoint) pingV2() (PingResult, error) {
   231  	logrus.Debugf("attempting v2 ping for registry endpoint %s", e)
   232  
   233  	req, err := http.NewRequest("GET", e.Path(""), nil)
   234  	if err != nil {
   235  		return PingResult{}, err
   236  	}
   237  
   238  	resp, err := e.client.Do(req)
   239  	if err != nil {
   240  		return PingResult{}, err
   241  	}
   242  	defer resp.Body.Close()
   243  
   244  	// The endpoint may have multiple supported versions.
   245  	// Ensure it supports the v2 Registry API.
   246  	var supportsV2 bool
   247  
   248  HeaderLoop:
   249  	for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
   250  		for _, versionName := range strings.Fields(supportedVersions) {
   251  			if versionName == "registry/2.0" {
   252  				supportsV2 = true
   253  				break HeaderLoop
   254  			}
   255  		}
   256  	}
   257  
   258  	if !supportsV2 {
   259  		return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e)
   260  	}
   261  
   262  	if resp.StatusCode == http.StatusOK {
   263  		// It would seem that no authentication/authorization is required.
   264  		// So we don't need to parse/add any authorization schemes.
   265  		return PingResult{Standalone: true}, nil
   266  	}
   267  
   268  	if resp.StatusCode == http.StatusUnauthorized {
   269  		// Parse the WWW-Authenticate Header and store the challenges
   270  		// on this endpoint object.
   271  		e.AuthChallenges = parseAuthHeader(resp.Header)
   272  		return PingResult{}, nil
   273  	}
   274  
   275  	return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
   276  }