github.com/dougm/docker@v1.5.0/registry/endpoint.go (about)

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