mirror of
https://github.com/containers/skopeo.git
synced 2025-05-07 07:26:47 +00:00
Vendor in the latest c/image to enforce blocking of registries when creating a c/image/docker.dockerClient. Add integration tests to avoid regressions. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
648 lines
23 KiB
Go
648 lines
23 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/containers/image/docker/reference"
|
|
"github.com/containers/image/pkg/docker/config"
|
|
"github.com/containers/image/pkg/sysregistriesv2"
|
|
"github.com/containers/image/pkg/tlsclientconfig"
|
|
"github.com/containers/image/types"
|
|
"github.com/docker/distribution/registry/client"
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
dockerHostname = "docker.io"
|
|
dockerV1Hostname = "index.docker.io"
|
|
dockerRegistry = "registry-1.docker.io"
|
|
|
|
resolvedPingV2URL = "%s://%s/v2/"
|
|
resolvedPingV1URL = "%s://%s/v1/_ping"
|
|
tagsPath = "/v2/%s/tags/list"
|
|
manifestPath = "/v2/%s/manifests/%s"
|
|
blobsPath = "/v2/%s/blobs/%s"
|
|
blobUploadPath = "/v2/%s/blobs/uploads/"
|
|
extensionsSignaturePath = "/extensions/v2/%s/signatures/%s"
|
|
|
|
minimumTokenLifetimeSeconds = 60
|
|
|
|
extensionSignatureSchemaVersion = 2 // extensionSignature.Version
|
|
extensionSignatureTypeAtomic = "atomic" // extensionSignature.Type
|
|
)
|
|
|
|
var (
|
|
// ErrV1NotSupported is returned when we're trying to talk to a
|
|
// docker V1 registry.
|
|
ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
|
|
// ErrUnauthorizedForCredentials is returned when the status code returned is 401
|
|
ErrUnauthorizedForCredentials = errors.New("unable to retrieve auth token: invalid username/password")
|
|
systemPerHostCertDirPaths = [2]string{"/etc/containers/certs.d", "/etc/docker/certs.d"}
|
|
)
|
|
|
|
// extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go:
|
|
// signature represents a Docker image signature.
|
|
type extensionSignature struct {
|
|
Version int `json:"schemaVersion"` // Version specifies the schema version
|
|
Name string `json:"name"` // Name must be in "sha256:<digest>@signatureName" format
|
|
Type string `json:"type"` // Type is optional, of not set it will be defaulted to "AtomicImageV1"
|
|
Content []byte `json:"content"` // Content contains the signature
|
|
}
|
|
|
|
// signatureList represents list of Docker image signatures.
|
|
type extensionSignatureList struct {
|
|
Signatures []extensionSignature `json:"signatures"`
|
|
}
|
|
|
|
type bearerToken struct {
|
|
Token string `json:"token"`
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
IssuedAt time.Time `json:"issued_at"`
|
|
expirationTime time.Time
|
|
}
|
|
|
|
// dockerClient is configuration for dealing with a single Docker registry.
|
|
type dockerClient struct {
|
|
// The following members are set by newDockerClient and do not change afterwards.
|
|
sys *types.SystemContext
|
|
registry string
|
|
|
|
// tlsClientConfig is setup by newDockerClient and will be used and updated
|
|
// by detectProperties(). Callers can edit tlsClientConfig.InsecureSkipVerify in the meantime.
|
|
tlsClientConfig *tls.Config
|
|
// The following members are not set by newDockerClient and must be set by callers if needed.
|
|
username string
|
|
password string
|
|
signatureBase signatureStorageBase
|
|
scope authScope
|
|
|
|
// The following members are detected registry properties:
|
|
// They are set after a successful detectProperties(), and never change afterwards.
|
|
client *http.Client
|
|
scheme string
|
|
challenges []challenge
|
|
supportsSignatures bool
|
|
|
|
// Private state for setupRequestAuth (key: string, value: bearerToken)
|
|
tokenCache sync.Map
|
|
// Private state for detectProperties:
|
|
detectPropertiesOnce sync.Once // detectPropertiesOnce is used to execute detectProperties() at most once.
|
|
detectPropertiesError error // detectPropertiesError caches the initial error.
|
|
}
|
|
|
|
type authScope struct {
|
|
remoteName string
|
|
actions string
|
|
}
|
|
|
|
// sendAuth determines whether we need authentication for v2 or v1 endpoint.
|
|
type sendAuth int
|
|
|
|
const (
|
|
// v2 endpoint with authentication.
|
|
v2Auth sendAuth = iota
|
|
// v1 endpoint with authentication.
|
|
// TODO: Get v1Auth working
|
|
// v1Auth
|
|
// no authentication, works for both v1 and v2.
|
|
noAuth
|
|
)
|
|
|
|
func newBearerTokenFromJSONBlob(blob []byte) (*bearerToken, error) {
|
|
token := new(bearerToken)
|
|
if err := json.Unmarshal(blob, &token); err != nil {
|
|
return nil, err
|
|
}
|
|
if token.Token == "" {
|
|
token.Token = token.AccessToken
|
|
}
|
|
if token.ExpiresIn < minimumTokenLifetimeSeconds {
|
|
token.ExpiresIn = minimumTokenLifetimeSeconds
|
|
logrus.Debugf("Increasing token expiration to: %d seconds", token.ExpiresIn)
|
|
}
|
|
if token.IssuedAt.IsZero() {
|
|
token.IssuedAt = time.Now().UTC()
|
|
}
|
|
token.expirationTime = token.IssuedAt.Add(time.Duration(token.ExpiresIn) * time.Second)
|
|
return token, nil
|
|
}
|
|
|
|
// this is cloned from docker/go-connections because upstream docker has changed
|
|
// it and make deps here fails otherwise.
|
|
// We'll drop this once we upgrade to docker 1.13.x deps.
|
|
func serverDefault() *tls.Config {
|
|
return &tls.Config{
|
|
// Avoid fallback to SSL protocols < TLS1.0
|
|
MinVersion: tls.VersionTLS10,
|
|
PreferServerCipherSuites: true,
|
|
CipherSuites: tlsconfig.DefaultServerAcceptedCiphers,
|
|
}
|
|
}
|
|
|
|
// dockerCertDir returns a path to a directory to be consumed by tlsclientconfig.SetupCertificates() depending on ctx and hostPort.
|
|
func dockerCertDir(sys *types.SystemContext, hostPort string) (string, error) {
|
|
if sys != nil && sys.DockerCertPath != "" {
|
|
return sys.DockerCertPath, nil
|
|
}
|
|
if sys != nil && sys.DockerPerHostCertDirPath != "" {
|
|
return filepath.Join(sys.DockerPerHostCertDirPath, hostPort), nil
|
|
}
|
|
|
|
var (
|
|
hostCertDir string
|
|
fullCertDirPath string
|
|
)
|
|
for _, systemPerHostCertDirPath := range systemPerHostCertDirPaths {
|
|
if sys != nil && sys.RootForImplicitAbsolutePaths != "" {
|
|
hostCertDir = filepath.Join(sys.RootForImplicitAbsolutePaths, systemPerHostCertDirPath)
|
|
} else {
|
|
hostCertDir = systemPerHostCertDirPath
|
|
}
|
|
|
|
fullCertDirPath = filepath.Join(hostCertDir, hostPort)
|
|
_, err := os.Stat(fullCertDirPath)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if os.IsPermission(err) {
|
|
logrus.Debugf("error accessing certs directory due to permissions: %v", err)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return fullCertDirPath, nil
|
|
}
|
|
|
|
// newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
|
|
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
|
|
func newDockerClientFromRef(sys *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
|
|
registry := reference.Domain(ref.ref)
|
|
username, password, err := config.GetAuthentication(sys, registry)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error getting username and password")
|
|
}
|
|
sigBase, err := configuredSignatureStorageBase(sys, ref, write)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := newDockerClient(sys, registry, ref.ref.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client.username = username
|
|
client.password = password
|
|
client.signatureBase = sigBase
|
|
client.scope.actions = actions
|
|
client.scope.remoteName = reference.Path(ref.ref)
|
|
return client, nil
|
|
}
|
|
|
|
// newDockerClient returns a new dockerClient instance for the given registry
|
|
// and reference. The reference is used to query the registry configuration
|
|
// and can either be a registry (e.g, "registry.com[:5000]"), a repository
|
|
// (e.g., "registry.com[:5000][/some/namespace]/repo").
|
|
// Please note that newDockerClient does not set all members of dockerClient
|
|
// (e.g., username and password); those must be set by callers if necessary.
|
|
func newDockerClient(sys *types.SystemContext, registry, reference string) (*dockerClient, error) {
|
|
hostName := registry
|
|
if registry == dockerHostname {
|
|
registry = dockerRegistry
|
|
}
|
|
tlsClientConfig := serverDefault()
|
|
|
|
// It is undefined whether the host[:port] string for dockerHostname should be dockerHostname or dockerRegistry,
|
|
// because docker/docker does not read the certs.d subdirectory at all in that case. We use the user-visible
|
|
// dockerHostname here, because it is more symmetrical to read the configuration in that case as well, and because
|
|
// generally the UI hides the existence of the different dockerRegistry. But note that this behavior is
|
|
// undocumented and may change if docker/docker changes.
|
|
certDir, err := dockerCertDir(sys, hostName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tlsclientconfig.SetupCertificates(certDir, tlsClientConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if TLS verification shall be skipped (default=false) which can
|
|
// be specified in the sysregistriesv2 configuration.
|
|
skipVerify := false
|
|
reg, err := sysregistriesv2.FindRegistry(sys, reference)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error loading registries")
|
|
}
|
|
if reg != nil {
|
|
if reg.Blocked {
|
|
return nil, fmt.Errorf("registry %s is blocked in %s", reg.Prefix, sysregistriesv2.ConfigPath(sys))
|
|
}
|
|
skipVerify = reg.Insecure
|
|
}
|
|
tlsClientConfig.InsecureSkipVerify = skipVerify
|
|
|
|
return &dockerClient{
|
|
sys: sys,
|
|
registry: registry,
|
|
tlsClientConfig: tlsClientConfig,
|
|
}, nil
|
|
}
|
|
|
|
// CheckAuth validates the credentials by attempting to log into the registry
|
|
// returns an error if an error occurred while making the http request or the status code received was 401
|
|
func CheckAuth(ctx context.Context, sys *types.SystemContext, username, password, registry string) error {
|
|
client, err := newDockerClient(sys, registry, registry)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error creating new docker client")
|
|
}
|
|
client.username = username
|
|
client.password = password
|
|
|
|
resp, err := client.makeRequest(ctx, "GET", "/v2/", nil, nil, v2Auth, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
return nil
|
|
case http.StatusUnauthorized:
|
|
return ErrUnauthorizedForCredentials
|
|
default:
|
|
return errors.Errorf("error occured with status code %d (%s)", resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
}
|
|
}
|
|
|
|
// SearchResult holds the information of each matching image
|
|
// It matches the output returned by the v1 endpoint
|
|
type SearchResult struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
// StarCount states the number of stars the image has
|
|
StarCount int `json:"star_count"`
|
|
IsTrusted bool `json:"is_trusted"`
|
|
// IsAutomated states whether the image is an automated build
|
|
IsAutomated bool `json:"is_automated"`
|
|
// IsOfficial states whether the image is an official build
|
|
IsOfficial bool `json:"is_official"`
|
|
}
|
|
|
|
// SearchRegistry queries a registry for images that contain "image" in their name
|
|
// The limit is the max number of results desired
|
|
// Note: The limit value doesn't work with all registries
|
|
// for example registry.access.redhat.com returns all the results without limiting it to the limit value
|
|
func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, image string, limit int) ([]SearchResult, error) {
|
|
type V2Results struct {
|
|
// Repositories holds the results returned by the /v2/_catalog endpoint
|
|
Repositories []string `json:"repositories"`
|
|
}
|
|
type V1Results struct {
|
|
// Results holds the results returned by the /v1/search endpoint
|
|
Results []SearchResult `json:"results"`
|
|
}
|
|
v2Res := &V2Results{}
|
|
v1Res := &V1Results{}
|
|
|
|
// Get credentials from authfile for the underlying hostname
|
|
username, password, err := config.GetAuthentication(sys, registry)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error getting username and password")
|
|
}
|
|
|
|
// The /v2/_catalog endpoint has been disabled for docker.io therefore
|
|
// the call made to that endpoint will fail. So using the v1 hostname
|
|
// for docker.io for simplicity of implementation and the fact that it
|
|
// returns search results.
|
|
hostname := registry
|
|
if registry == dockerHostname {
|
|
hostname = dockerV1Hostname
|
|
}
|
|
|
|
client, err := newDockerClient(sys, hostname, registry)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error creating new docker client")
|
|
}
|
|
client.username = username
|
|
client.password = password
|
|
|
|
// Only try the v1 search endpoint if the search query is not empty. If it is
|
|
// empty skip to the v2 endpoint.
|
|
if image != "" {
|
|
// set up the query values for the v1 endpoint
|
|
u := url.URL{
|
|
Path: "/v1/search",
|
|
}
|
|
q := u.Query()
|
|
q.Set("q", image)
|
|
q.Set("n", strconv.Itoa(limit))
|
|
u.RawQuery = q.Encode()
|
|
|
|
logrus.Debugf("trying to talk to v1 search endpoint")
|
|
resp, err := client.makeRequest(ctx, "GET", u.String(), nil, nil, noAuth, nil)
|
|
if err != nil {
|
|
logrus.Debugf("error getting search results from v1 endpoint %q: %v", registry, err)
|
|
} else {
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
logrus.Debugf("error getting search results from v1 endpoint %q, status code %d (%s)", registry, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
} else {
|
|
if err := json.NewDecoder(resp.Body).Decode(v1Res); err != nil {
|
|
return nil, err
|
|
}
|
|
return v1Res.Results, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
logrus.Debugf("trying to talk to v2 search endpoint")
|
|
resp, err := client.makeRequest(ctx, "GET", "/v2/_catalog", nil, nil, v2Auth, nil)
|
|
if err != nil {
|
|
logrus.Debugf("error getting search results from v2 endpoint %q: %v", registry, err)
|
|
} else {
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
logrus.Errorf("error getting search results from v2 endpoint %q, status code %d (%s)", registry, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
} else {
|
|
if err := json.NewDecoder(resp.Body).Decode(v2Res); err != nil {
|
|
return nil, err
|
|
}
|
|
searchRes := []SearchResult{}
|
|
for _, repo := range v2Res.Repositories {
|
|
if strings.Contains(repo, image) {
|
|
res := SearchResult{
|
|
Name: repo,
|
|
}
|
|
searchRes = append(searchRes, res)
|
|
}
|
|
}
|
|
return searchRes, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Wrapf(err, "couldn't search registry %q", registry)
|
|
}
|
|
|
|
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
|
|
// The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/.
|
|
func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader, auth sendAuth, extraScope *authScope) (*http.Response, error) {
|
|
if err := c.detectProperties(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := fmt.Sprintf("%s://%s%s", c.scheme, c.registry, path)
|
|
return c.makeRequestToResolvedURL(ctx, method, url, headers, stream, -1, auth, extraScope)
|
|
}
|
|
|
|
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
|
|
// streamLen, if not -1, specifies the length of the data expected on stream.
|
|
// makeRequest should generally be preferred.
|
|
// TODO(runcom): too many arguments here, use a struct
|
|
func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method, url string, headers map[string][]string, stream io.Reader, streamLen int64, auth sendAuth, extraScope *authScope) (*http.Response, error) {
|
|
req, err := http.NewRequest(method, url, stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req = req.WithContext(ctx)
|
|
if streamLen != -1 { // Do not blindly overwrite if streamLen == -1, http.NewRequest above can figure out the length of bytes.Reader and similar objects without us having to compute it.
|
|
req.ContentLength = streamLen
|
|
}
|
|
req.Header.Set("Docker-Distribution-API-Version", "registry/2.0")
|
|
for n, h := range headers {
|
|
for _, hh := range h {
|
|
req.Header.Add(n, hh)
|
|
}
|
|
}
|
|
if c.sys != nil && c.sys.DockerRegistryUserAgent != "" {
|
|
req.Header.Add("User-Agent", c.sys.DockerRegistryUserAgent)
|
|
}
|
|
if auth == v2Auth {
|
|
if err := c.setupRequestAuth(req, extraScope); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
logrus.Debugf("%s %s", method, url)
|
|
res, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// we're using the challenges from the /v2/ ping response and not the one from the destination
|
|
// URL in this request because:
|
|
//
|
|
// 1) docker does that as well
|
|
// 2) gcr.io is sending 401 without a WWW-Authenticate header in the real request
|
|
//
|
|
// debugging: https://github.com/containers/image/pull/211#issuecomment-273426236 and follows up
|
|
func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope) error {
|
|
if len(c.challenges) == 0 {
|
|
return nil
|
|
}
|
|
schemeNames := make([]string, 0, len(c.challenges))
|
|
for _, challenge := range c.challenges {
|
|
schemeNames = append(schemeNames, challenge.Scheme)
|
|
switch challenge.Scheme {
|
|
case "basic":
|
|
req.SetBasicAuth(c.username, c.password)
|
|
return nil
|
|
case "bearer":
|
|
cacheKey := ""
|
|
scopes := []authScope{c.scope}
|
|
if extraScope != nil {
|
|
// Using ':' as a separator here is unambiguous because getBearerToken below uses the same separator when formatting a remote request (and because repository names can't contain colons).
|
|
cacheKey = fmt.Sprintf("%s:%s", extraScope.remoteName, extraScope.actions)
|
|
scopes = append(scopes, *extraScope)
|
|
}
|
|
var token bearerToken
|
|
t, inCache := c.tokenCache.Load(cacheKey)
|
|
if inCache {
|
|
token = t.(bearerToken)
|
|
}
|
|
if !inCache || time.Now().After(token.expirationTime) {
|
|
t, err := c.getBearerToken(req.Context(), challenge, scopes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token = *t
|
|
c.tokenCache.Store(cacheKey, token)
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Token))
|
|
return nil
|
|
default:
|
|
logrus.Debugf("no handler for %s authentication", challenge.Scheme)
|
|
}
|
|
}
|
|
logrus.Infof("None of the challenges sent by server (%s) are supported, trying an unauthenticated request anyway", strings.Join(schemeNames, ", "))
|
|
return nil
|
|
}
|
|
|
|
func (c *dockerClient) getBearerToken(ctx context.Context, challenge challenge, scopes []authScope) (*bearerToken, error) {
|
|
realm, ok := challenge.Parameters["realm"]
|
|
if !ok {
|
|
return nil, errors.Errorf("missing realm in bearer auth challenge")
|
|
}
|
|
|
|
authReq, err := http.NewRequest("GET", realm, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authReq = authReq.WithContext(ctx)
|
|
getParams := authReq.URL.Query()
|
|
if c.username != "" {
|
|
getParams.Add("account", c.username)
|
|
}
|
|
if service, ok := challenge.Parameters["service"]; ok && service != "" {
|
|
getParams.Add("service", service)
|
|
}
|
|
for _, scope := range scopes {
|
|
if scope.remoteName != "" && scope.actions != "" {
|
|
getParams.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
|
|
}
|
|
}
|
|
authReq.URL.RawQuery = getParams.Encode()
|
|
if c.username != "" && c.password != "" {
|
|
authReq.SetBasicAuth(c.username, c.password)
|
|
}
|
|
logrus.Debugf("%s %s", authReq.Method, authReq.URL.String())
|
|
tr := tlsclientconfig.NewTransport()
|
|
// TODO(runcom): insecure for now to contact the external token service
|
|
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
client := &http.Client{Transport: tr}
|
|
res, err := client.Do(authReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
switch res.StatusCode {
|
|
case http.StatusUnauthorized:
|
|
return nil, ErrUnauthorizedForCredentials
|
|
case http.StatusOK:
|
|
break
|
|
default:
|
|
return nil, errors.Errorf("unexpected http code: %d (%s), URL: %s", res.StatusCode, http.StatusText(res.StatusCode), authReq.URL)
|
|
}
|
|
tokenBlob, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newBearerTokenFromJSONBlob(tokenBlob)
|
|
}
|
|
|
|
// detectPropertiesHelper performs the work of detectProperties which executes
|
|
// it at most once.
|
|
func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error {
|
|
// We overwrite the TLS clients `InsecureSkipVerify` only if explicitly
|
|
// specified by the system context
|
|
if c.sys != nil && c.sys.DockerInsecureSkipTLSVerify != types.OptionalBoolUndefined {
|
|
c.tlsClientConfig.InsecureSkipVerify = c.sys.DockerInsecureSkipTLSVerify == types.OptionalBoolTrue
|
|
}
|
|
tr := tlsclientconfig.NewTransport()
|
|
tr.TLSClientConfig = c.tlsClientConfig
|
|
c.client = &http.Client{Transport: tr}
|
|
|
|
ping := func(scheme string) error {
|
|
url := fmt.Sprintf(resolvedPingV2URL, scheme, c.registry)
|
|
resp, err := c.makeRequestToResolvedURL(ctx, "GET", url, nil, nil, -1, noAuth, nil)
|
|
if err != nil {
|
|
logrus.Debugf("Ping %s err %s (%#v)", url, err.Error(), err)
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
logrus.Debugf("Ping %s status %d", url, resp.StatusCode)
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
|
return errors.Errorf("error pinging registry %s, response code %d (%s)", c.registry, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
}
|
|
c.challenges = parseAuthHeader(resp.Header)
|
|
c.scheme = scheme
|
|
c.supportsSignatures = resp.Header.Get("X-Registry-Supports-Signatures") == "1"
|
|
return nil
|
|
}
|
|
err := ping("https")
|
|
if err != nil && c.tlsClientConfig.InsecureSkipVerify {
|
|
err = ping("http")
|
|
}
|
|
if err != nil {
|
|
err = errors.Wrap(err, "pinging docker registry returned")
|
|
if c.sys != nil && c.sys.DockerDisableV1Ping {
|
|
return err
|
|
}
|
|
// best effort to understand if we're talking to a V1 registry
|
|
pingV1 := func(scheme string) bool {
|
|
url := fmt.Sprintf(resolvedPingV1URL, scheme, c.registry)
|
|
resp, err := c.makeRequestToResolvedURL(ctx, "GET", url, nil, nil, -1, noAuth, nil)
|
|
if err != nil {
|
|
logrus.Debugf("Ping %s err %s (%#v)", url, err.Error(), err)
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
logrus.Debugf("Ping %s status %d", url, resp.StatusCode)
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
isV1 := pingV1("https")
|
|
if !isV1 && c.tlsClientConfig.InsecureSkipVerify {
|
|
isV1 = pingV1("http")
|
|
}
|
|
if isV1 {
|
|
err = ErrV1NotSupported
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// detectProperties detects various properties of the registry.
|
|
// See the dockerClient documentation for members which are affected by this.
|
|
func (c *dockerClient) detectProperties(ctx context.Context) error {
|
|
c.detectPropertiesOnce.Do(func() { c.detectPropertiesError = c.detectPropertiesHelper(ctx) })
|
|
return c.detectPropertiesError
|
|
}
|
|
|
|
// getExtensionsSignatures returns signatures from the X-Registry-Supports-Signatures API extension,
|
|
// using the original data structures.
|
|
func (c *dockerClient) getExtensionsSignatures(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) (*extensionSignatureList, error) {
|
|
path := fmt.Sprintf(extensionsSignaturePath, reference.Path(ref.ref), manifestDigest)
|
|
res, err := c.makeRequest(ctx, "GET", path, nil, nil, v2Auth, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, errors.Wrapf(client.HandleErrorResponse(res), "Error downloading signatures for %s in %s", manifestDigest, ref.ref.Name())
|
|
}
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var parsedBody extensionSignatureList
|
|
if err := json.Unmarshal(body, &parsedBody); err != nil {
|
|
return nil, errors.Wrapf(err, "Error decoding signature list")
|
|
}
|
|
return &parsedBody, nil
|
|
}
|