caee4a99c9
Vendor updated containers/image and containers/storage, along with any new dependencies they drag in, and updated versions of other dependencies that happen to get pulled in. github.com/coreos/go-systemd/daemon/SdNotify() now takes a boolean to control whether or not it unsets the NOTIFY_SOCKET variable from the calling process's environment. Adapt. github.com/opencontainers/runtime-tools/generate/Generator.AddProcessEnv() now takes the environment variable name and value as two arguments, not one. Adapt. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
540 lines
17 KiB
Go
540 lines
17 KiB
Go
package docker
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/containers/image/types"
|
|
"github.com/containers/storage/pkg/homedir"
|
|
"github.com/docker/go-connections/sockets"
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
dockerHostname = "docker.io"
|
|
dockerRegistry = "registry-1.docker.io"
|
|
dockerAuthRegistry = "https://index.docker.io/v1/"
|
|
|
|
dockerCfg = ".docker"
|
|
dockerCfgFileName = "config.json"
|
|
dockerCfgObsolete = ".dockercfg"
|
|
|
|
baseURL = "%s://%s/v2/"
|
|
baseURLV1 = "%s://%s/v1/_ping"
|
|
tagsURL = "%s/tags/list"
|
|
manifestURL = "%s/manifests/%s"
|
|
blobsURL = "%s/blobs/%s"
|
|
blobUploadURL = "%s/blobs/uploads/"
|
|
)
|
|
|
|
// ErrV1NotSupported is returned when we're trying to talk to a
|
|
// docker V1 registry.
|
|
var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
|
|
|
|
// dockerClient is configuration for dealing with a single Docker registry.
|
|
type dockerClient struct {
|
|
ctx *types.SystemContext
|
|
registry string
|
|
username string
|
|
password string
|
|
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
|
|
scheme string // Cache of a value returned by a successful ping() if not empty
|
|
client *http.Client
|
|
signatureBase signatureStorageBase
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
func newTransport() *http.Transport {
|
|
direct := &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}
|
|
tr := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
Dial: direct.Dial,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
|
|
DisableKeepAlives: true,
|
|
}
|
|
proxyDialer, err := sockets.DialerFromEnvironment(direct)
|
|
if err == nil {
|
|
tr.Dial = proxyDialer.Dial
|
|
}
|
|
return tr
|
|
}
|
|
|
|
func setupCertificates(dir string, tlsc *tls.Config) error {
|
|
if dir == "" {
|
|
return nil
|
|
}
|
|
fs, err := ioutil.ReadDir(dir)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
for _, f := range fs {
|
|
fullPath := filepath.Join(dir, f.Name())
|
|
if strings.HasSuffix(f.Name(), ".crt") {
|
|
systemPool, err := tlsconfig.SystemCertPool()
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to get system cert pool")
|
|
}
|
|
tlsc.RootCAs = systemPool
|
|
logrus.Debugf("crt: %s", fullPath)
|
|
data, err := ioutil.ReadFile(fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tlsc.RootCAs.AppendCertsFromPEM(data)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".cert") {
|
|
certName := f.Name()
|
|
keyName := certName[:len(certName)-5] + ".key"
|
|
logrus.Debugf("cert: %s", fullPath)
|
|
if !hasFile(fs, keyName) {
|
|
return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName)
|
|
}
|
|
cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tlsc.Certificates = append(tlsc.Certificates, cert)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".key") {
|
|
keyName := f.Name()
|
|
certName := keyName[:len(keyName)-4] + ".cert"
|
|
logrus.Debugf("key: %s", fullPath)
|
|
if !hasFile(fs, certName) {
|
|
return errors.Errorf("missing client certificate %s for key %s", certName, keyName)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasFile(files []os.FileInfo, name string) bool {
|
|
for _, f := range files {
|
|
if f.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// newDockerClient 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 newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool) (*dockerClient, error) {
|
|
registry := ref.ref.Hostname()
|
|
if registry == dockerHostname {
|
|
registry = dockerRegistry
|
|
}
|
|
username, password, err := getAuth(ctx, ref.ref.Hostname())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr := newTransport()
|
|
if ctx != nil && (ctx.DockerCertPath != "" || ctx.DockerInsecureSkipTLSVerify) {
|
|
tlsc := &tls.Config{}
|
|
|
|
if err := setupCertificates(ctx.DockerCertPath, tlsc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsc.InsecureSkipVerify = ctx.DockerInsecureSkipTLSVerify
|
|
tr.TLSClientConfig = tlsc
|
|
}
|
|
if tr.TLSClientConfig == nil {
|
|
tr.TLSClientConfig = serverDefault()
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dockerClient{
|
|
ctx: ctx,
|
|
registry: registry,
|
|
username: username,
|
|
password: password,
|
|
client: client,
|
|
signatureBase: sigBase,
|
|
}, nil
|
|
}
|
|
|
|
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
|
|
// url is NOT an absolute URL, but a path relative to the /v2/ top-level API path. The host name and schema is taken from the client or autodetected.
|
|
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
|
|
if c.scheme == "" {
|
|
pr, err := c.ping()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.wwwAuthenticate = pr.WWWAuthenticate
|
|
c.scheme = pr.scheme
|
|
}
|
|
|
|
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
|
|
return c.makeRequestToResolvedURL(method, url, headers, stream, -1, true)
|
|
}
|
|
|
|
// 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(method, url string, headers map[string][]string, stream io.Reader, streamLen int64, sendAuth bool) (*http.Response, error) {
|
|
req, err := http.NewRequest(method, url, stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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.ctx != nil && c.ctx.DockerRegistryUserAgent != "" {
|
|
req.Header.Add("User-Agent", c.ctx.DockerRegistryUserAgent)
|
|
}
|
|
if c.wwwAuthenticate != "" && sendAuth {
|
|
if err := c.setupRequestAuth(req); 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
|
|
}
|
|
|
|
func (c *dockerClient) setupRequestAuth(req *http.Request) error {
|
|
tokens := strings.SplitN(strings.TrimSpace(c.wwwAuthenticate), " ", 2)
|
|
if len(tokens) != 2 {
|
|
return errors.Errorf("expected 2 tokens in WWW-Authenticate: %d, %s", len(tokens), c.wwwAuthenticate)
|
|
}
|
|
switch tokens[0] {
|
|
case "Basic":
|
|
req.SetBasicAuth(c.username, c.password)
|
|
return nil
|
|
case "Bearer":
|
|
// FIXME? This gets a new token for every API request;
|
|
// we may be easily able to reuse a previous token, e.g.
|
|
// for OpenShift the token only identifies the user and does not vary
|
|
// across operations. Should we just try the request first, and
|
|
// only get a new token on failure?
|
|
// OTOH what to do with the single-use body stream in that case?
|
|
|
|
// Try performing the request, expecting it to fail.
|
|
testReq := *req
|
|
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
|
|
testReq.Body = nil
|
|
testReq.ContentLength = 0
|
|
res, err := c.client.Do(&testReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chs := parseAuthHeader(res.Header)
|
|
// We could end up in this "if" statement if the /v2/ call (during ping)
|
|
// returned 401 with a valid WWW-Authenticate=Bearer header.
|
|
// That doesn't **always** mean, however, that the specific API request
|
|
// (different from /v2/) actually needs to be authorized.
|
|
// One example of this _weird_ scenario happens with GCR.io docker
|
|
// registries.
|
|
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
|
|
// With gcr.io, the /v2/ call returns a 401 with a valid WWW-Authenticate=Bearer
|
|
// header but the repository could be _public_ (no authorization is needed).
|
|
// Hence, the registry response contains no challenges and the status
|
|
// code is not 401.
|
|
// We just skip this case as it's not standard on docker/distribution
|
|
// registries (https://github.com/docker/distribution/blob/master/docs/spec/api.md#api-version-check)
|
|
if res.StatusCode != http.StatusUnauthorized {
|
|
return nil
|
|
}
|
|
// gcr.io private repositories pull instead requires us to send user:pass pair in
|
|
// order to retrieve a token and setup the correct Bearer token.
|
|
// try again one last time with Basic Auth
|
|
testReq2 := *req
|
|
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
|
|
testReq2.Body = nil
|
|
testReq2.ContentLength = 0
|
|
testReq2.SetBasicAuth(c.username, c.password)
|
|
res, err := c.client.Do(&testReq2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chs = parseAuthHeader(res.Header)
|
|
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
|
|
// no need for bearer? wtf?
|
|
return nil
|
|
}
|
|
}
|
|
// Arbitrarily use the first challenge, there is no reason to expect more than one.
|
|
challenge := chs[0]
|
|
if challenge.Scheme != "bearer" { // Another artifact of trying to handle WWW-Authenticate before it actually happens.
|
|
return errors.Errorf("Unimplemented: WWW-Authenticate Bearer replaced by %#v", challenge.Scheme)
|
|
}
|
|
realm, ok := challenge.Parameters["realm"]
|
|
if !ok {
|
|
return errors.Errorf("missing realm in bearer auth challenge")
|
|
}
|
|
service, _ := challenge.Parameters["service"] // Will be "" if not present
|
|
scope, _ := challenge.Parameters["scope"] // Will be "" if not present
|
|
token, err := c.getBearerToken(realm, service, scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
return nil
|
|
}
|
|
return errors.Errorf("no handler for %s authentication", tokens[0])
|
|
// support docker bearer with authconfig's Auth string? see docker2aci
|
|
}
|
|
|
|
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
|
|
authReq, err := http.NewRequest("GET", realm, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
getParams := authReq.URL.Query()
|
|
if service != "" {
|
|
getParams.Add("service", service)
|
|
}
|
|
if scope != "" {
|
|
getParams.Add("scope", scope)
|
|
}
|
|
authReq.URL.RawQuery = getParams.Encode()
|
|
if c.username != "" && c.password != "" {
|
|
authReq.SetBasicAuth(c.username, c.password)
|
|
}
|
|
tr := 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 "", err
|
|
}
|
|
defer res.Body.Close()
|
|
switch res.StatusCode {
|
|
case http.StatusUnauthorized:
|
|
return "", errors.Errorf("unable to retrieve auth token: 401 unauthorized")
|
|
case http.StatusOK:
|
|
break
|
|
default:
|
|
return "", errors.Errorf("unexpected http code: %d, URL: %s", res.StatusCode, authReq.URL)
|
|
}
|
|
tokenBlob, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tokenStruct := struct {
|
|
Token string `json:"token"`
|
|
}{}
|
|
if err := json.Unmarshal(tokenBlob, &tokenStruct); err != nil {
|
|
return "", err
|
|
}
|
|
// TODO(runcom): reuse tokens?
|
|
//hostAuthTokens, ok = rb.hostsV2AuthTokens[req.URL.Host]
|
|
//if !ok {
|
|
//hostAuthTokens = make(map[string]string)
|
|
//rb.hostsV2AuthTokens[req.URL.Host] = hostAuthTokens
|
|
//}
|
|
//hostAuthTokens[repo] = tokenStruct.Token
|
|
return tokenStruct.Token, nil
|
|
}
|
|
|
|
func getAuth(ctx *types.SystemContext, registry string) (string, string, error) {
|
|
if ctx != nil && ctx.DockerAuthConfig != nil {
|
|
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
|
|
}
|
|
var dockerAuth dockerConfigFile
|
|
dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName)
|
|
if _, err := os.Stat(dockerCfgPath); err == nil {
|
|
j, err := ioutil.ReadFile(dockerCfgPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := json.Unmarshal(j, &dockerAuth); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
} else if os.IsNotExist(err) {
|
|
// try old config path
|
|
oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete))
|
|
if _, err := os.Stat(oldDockerCfgPath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", "", nil
|
|
}
|
|
return "", "", errors.Wrap(err, oldDockerCfgPath)
|
|
}
|
|
|
|
j, err := ioutil.ReadFile(oldDockerCfgPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
} else if err != nil {
|
|
return "", "", errors.Wrap(err, dockerCfgPath)
|
|
}
|
|
|
|
// I'm feeling lucky
|
|
if c, exists := dockerAuth.AuthConfigs[registry]; exists {
|
|
return decodeDockerAuth(c.Auth)
|
|
}
|
|
|
|
// bad luck; let's normalize the entries first
|
|
registry = normalizeRegistry(registry)
|
|
normalizedAuths := map[string]dockerAuthConfig{}
|
|
for k, v := range dockerAuth.AuthConfigs {
|
|
normalizedAuths[normalizeRegistry(k)] = v
|
|
}
|
|
if c, exists := normalizedAuths[registry]; exists {
|
|
return decodeDockerAuth(c.Auth)
|
|
}
|
|
return "", "", nil
|
|
}
|
|
|
|
type pingResponse struct {
|
|
WWWAuthenticate string
|
|
APIVersion string
|
|
scheme string
|
|
}
|
|
|
|
func (c *dockerClient) ping() (*pingResponse, error) {
|
|
ping := func(scheme string) (*pingResponse, error) {
|
|
url := fmt.Sprintf(baseURL, scheme, c.registry)
|
|
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
|
|
logrus.Debugf("Ping %s err %#v", url, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
|
return nil, errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
|
|
}
|
|
pr := &pingResponse{}
|
|
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
|
|
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
|
|
pr.scheme = scheme
|
|
return pr, nil
|
|
}
|
|
pr, err := ping("https")
|
|
if err != nil && c.ctx != nil && c.ctx.DockerInsecureSkipTLSVerify {
|
|
pr, err = ping("http")
|
|
}
|
|
if err != nil {
|
|
err = errors.Wrap(err, "pinging docker registry returned")
|
|
if c.ctx != nil && c.ctx.DockerDisableV1Ping {
|
|
return nil, err
|
|
}
|
|
// best effort to understand if we're talking to a V1 registry
|
|
pingV1 := func(scheme string) bool {
|
|
url := fmt.Sprintf(baseURLV1, scheme, c.registry)
|
|
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
|
|
logrus.Debugf("Ping %s err %#v", url, err)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v1/_ping", resp.StatusCode)
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
isV1 := pingV1("https")
|
|
if !isV1 && c.ctx != nil && c.ctx.DockerInsecureSkipTLSVerify {
|
|
isV1 = pingV1("http")
|
|
}
|
|
if isV1 {
|
|
err = ErrV1NotSupported
|
|
}
|
|
}
|
|
return pr, err
|
|
}
|
|
|
|
func getDefaultConfigDir(confPath string) string {
|
|
return filepath.Join(homedir.Get(), confPath)
|
|
}
|
|
|
|
type dockerAuthConfig struct {
|
|
Auth string `json:"auth,omitempty"`
|
|
}
|
|
|
|
type dockerConfigFile struct {
|
|
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
|
}
|
|
|
|
func decodeDockerAuth(s string) (string, string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(s)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
parts := strings.SplitN(string(decoded), ":", 2)
|
|
if len(parts) != 2 {
|
|
// if it's invalid just skip, as docker does
|
|
return "", "", nil
|
|
}
|
|
user := parts[0]
|
|
password := strings.Trim(parts[1], "\x00")
|
|
return user, password, nil
|
|
}
|
|
|
|
// convertToHostname converts a registry url which has http|https prepended
|
|
// to just an hostname.
|
|
// Copied from github.com/docker/docker/registry/auth.go
|
|
func convertToHostname(url string) string {
|
|
stripped := url
|
|
if strings.HasPrefix(url, "http://") {
|
|
stripped = strings.TrimPrefix(url, "http://")
|
|
} else if strings.HasPrefix(url, "https://") {
|
|
stripped = strings.TrimPrefix(url, "https://")
|
|
}
|
|
|
|
nameParts := strings.SplitN(stripped, "/", 2)
|
|
|
|
return nameParts[0]
|
|
}
|
|
|
|
func normalizeRegistry(registry string) string {
|
|
normalized := convertToHostname(registry)
|
|
switch normalized {
|
|
case "registry-1.docker.io", "docker.io":
|
|
return "index.docker.io"
|
|
}
|
|
return normalized
|
|
}
|