2016-07-20 09:46:01 +00:00
package docker
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
2016-11-22 19:32:10 +00:00
"net"
2016-07-20 09:46:01 +00:00
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
2016-09-17 13:50:35 +00:00
"github.com/containers/image/types"
2016-11-22 19:32:10 +00:00
"github.com/containers/storage/pkg/homedir"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
2016-10-17 13:53:40 +00:00
"github.com/pkg/errors"
2016-07-20 09:46:01 +00:00
)
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/"
2016-11-22 19:32:10 +00:00
baseURLV1 = "%s://%s/v1/_ping"
2016-07-20 09:46:01 +00:00
tagsURL = "%s/tags/list"
manifestURL = "%s/manifests/%s"
blobsURL = "%s/blobs/%s"
blobUploadURL = "%s/blobs/uploads/"
2017-02-03 19:15:09 +00:00
minimumTokenLifetimeSeconds = 60
2016-07-20 09:46:01 +00:00
)
2016-11-22 19:32:10 +00:00
// 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" )
2017-02-03 19:15:09 +00:00
type bearerToken struct {
Token string ` json:"token" `
ExpiresIn int ` json:"expires_in" `
IssuedAt time . Time ` json:"issued_at" `
}
2016-07-20 09:46:01 +00:00
// dockerClient is configuration for dealing with a single Docker registry.
type dockerClient struct {
2017-02-03 19:15:09 +00:00
ctx * types . SystemContext
registry string
username string
password string
scheme string // Cache of a value returned by a successful ping() if not empty
client * http . Client
signatureBase signatureStorageBase
challenges [ ] challenge
scope authScope
token * bearerToken
tokenExpiration time . Time
2017-02-01 00:45:59 +00:00
}
type authScope struct {
remoteName string
actions string
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
// 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 {
2016-10-17 13:53:40 +00:00
return errors . Wrap ( err , "unable to get system cert pool" )
2016-11-22 19:32:10 +00:00
}
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 ) {
2016-10-17 13:53:40 +00:00
return errors . Errorf ( "missing key %s for client certificate %s. Note that CA certificates should use the extension .crt" , keyName , certName )
2016-11-22 19:32:10 +00:00
}
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 ) {
2016-10-17 13:53:40 +00:00
return errors . Errorf ( "missing client certificate %s for key %s" , certName , keyName )
2016-11-22 19:32:10 +00:00
}
}
}
return nil
}
func hasFile ( files [ ] os . FileInfo , name string ) bool {
for _ , f := range files {
if f . Name ( ) == name {
return true
}
}
return false
}
2016-07-20 09:46:01 +00:00
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
2016-09-17 13:50:35 +00:00
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
2017-02-01 00:45:59 +00:00
func newDockerClient ( ctx * types . SystemContext , ref dockerReference , write bool , actions string ) ( * dockerClient , error ) {
2016-09-17 13:50:35 +00:00
registry := ref . ref . Hostname ( )
if registry == dockerHostname {
2016-07-20 09:46:01 +00:00
registry = dockerRegistry
}
2016-11-22 19:32:10 +00:00
username , password , err := getAuth ( ctx , ref . ref . Hostname ( ) )
2016-07-20 09:46:01 +00:00
if err != nil {
return nil , err
}
2016-11-22 19:32:10 +00:00
tr := newTransport ( )
2016-09-17 13:50:35 +00:00
if ctx != nil && ( ctx . DockerCertPath != "" || ctx . DockerInsecureSkipTLSVerify ) {
2016-07-20 09:46:01 +00:00
tlsc := & tls . Config { }
2016-11-22 19:32:10 +00:00
if err := setupCertificates ( ctx . DockerCertPath , tlsc ) ; err != nil {
return nil , err
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
2016-09-17 13:50:35 +00:00
tlsc . InsecureSkipVerify = ctx . DockerInsecureSkipTLSVerify
2016-11-22 19:32:10 +00:00
tr . TLSClientConfig = tlsc
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
if tr . TLSClientConfig == nil {
tr . TLSClientConfig = serverDefault ( )
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
client := & http . Client { Transport : tr }
2016-09-17 13:50:35 +00:00
sigBase , err := configuredSignatureStorageBase ( ctx , ref , write )
if err != nil {
return nil , err
}
2016-07-20 09:46:01 +00:00
return & dockerClient {
2016-09-17 13:50:35 +00:00
ctx : ctx ,
registry : registry ,
username : username ,
password : password ,
client : client ,
signatureBase : sigBase ,
2017-02-01 00:45:59 +00:00
scope : authScope {
actions : actions ,
remoteName : ref . ref . RemoteName ( ) ,
} ,
2016-07-20 09:46:01 +00:00
} , 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 == "" {
2017-02-01 00:45:59 +00:00
if err := c . ping ( ) ; err != nil {
2016-07-20 09:46:01 +00:00
return nil , err
}
}
url = fmt . Sprintf ( baseURL , c . scheme , c . registry ) + url
2016-11-22 19:32:10 +00:00
return c . makeRequestToResolvedURL ( method , url , headers , stream , - 1 , true )
2016-07-20 09:46:01 +00:00
}
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
2016-09-17 13:50:35 +00:00
// streamLen, if not -1, specifies the length of the data expected on stream.
2016-07-20 09:46:01 +00:00
// makeRequest should generally be preferred.
2016-11-22 19:32:10 +00:00
// 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 ) {
2016-07-20 09:46:01 +00:00
req , err := http . NewRequest ( method , url , stream )
if err != nil {
return nil , err
}
2016-09-17 13:50:35 +00:00
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
}
2016-07-20 09:46:01 +00:00
req . Header . Set ( "Docker-Distribution-API-Version" , "registry/2.0" )
for n , h := range headers {
for _ , hh := range h {
req . Header . Add ( n , hh )
}
}
2016-11-22 19:32:10 +00:00
if c . ctx != nil && c . ctx . DockerRegistryUserAgent != "" {
req . Header . Add ( "User-Agent" , c . ctx . DockerRegistryUserAgent )
}
2017-02-01 00:45:59 +00:00
if sendAuth {
2016-07-20 09:46:01 +00:00
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
}
2017-02-01 00:45:59 +00:00
// 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
2016-07-20 09:46:01 +00:00
func ( c * dockerClient ) setupRequestAuth ( req * http . Request ) error {
2017-02-01 00:45:59 +00:00
if len ( c . challenges ) == 0 {
return nil
2016-07-20 09:46:01 +00:00
}
2017-02-01 00:45:59 +00:00
// assume just one...
challenge := c . challenges [ 0 ]
switch challenge . Scheme {
case "basic" :
2016-07-20 09:46:01 +00:00
req . SetBasicAuth ( c . username , c . password )
return nil
2017-02-01 00:45:59 +00:00
case "bearer" :
2017-02-03 19:15:09 +00:00
if c . token == nil || time . Now ( ) . After ( c . tokenExpiration ) {
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 := fmt . Sprintf ( "repository:%s:%s" , c . scope . remoteName , c . scope . actions )
token , err := c . getBearerToken ( realm , service , scope )
if err != nil {
return err
}
c . token = token
c . tokenExpiration = token . IssuedAt . Add ( time . Duration ( token . ExpiresIn ) * time . Second )
2016-07-20 09:46:01 +00:00
}
2017-02-03 19:15:09 +00:00
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , c . token . Token ) )
2016-07-20 09:46:01 +00:00
return nil
}
2017-02-01 00:45:59 +00:00
return errors . Errorf ( "no handler for %s authentication" , challenge . Scheme )
2016-07-20 09:46:01 +00:00
}
2017-02-03 19:15:09 +00:00
func ( c * dockerClient ) getBearerToken ( realm , service , scope string ) ( * bearerToken , error ) {
2016-07-20 09:46:01 +00:00
authReq , err := http . NewRequest ( "GET" , realm , nil )
if err != nil {
2017-02-03 19:15:09 +00:00
return nil , err
2016-07-20 09:46:01 +00:00
}
getParams := authReq . URL . Query ( )
2016-09-17 13:50:35 +00:00
if service != "" {
getParams . Add ( "service" , service )
}
2016-07-20 09:46:01 +00:00
if scope != "" {
getParams . Add ( "scope" , scope )
}
authReq . URL . RawQuery = getParams . Encode ( )
if c . username != "" && c . password != "" {
authReq . SetBasicAuth ( c . username , c . password )
}
2016-11-22 19:32:10 +00:00
tr := newTransport ( )
// TODO(runcom): insecure for now to contact the external token service
tr . TLSClientConfig = & tls . Config { InsecureSkipVerify : true }
2016-07-20 09:46:01 +00:00
client := & http . Client { Transport : tr }
res , err := client . Do ( authReq )
if err != nil {
2017-02-03 19:15:09 +00:00
return nil , err
2016-07-20 09:46:01 +00:00
}
defer res . Body . Close ( )
switch res . StatusCode {
case http . StatusUnauthorized :
2017-02-03 19:15:09 +00:00
return nil , errors . Errorf ( "unable to retrieve auth token: 401 unauthorized" )
2016-07-20 09:46:01 +00:00
case http . StatusOK :
break
default :
2017-02-03 19:15:09 +00:00
return nil , errors . Errorf ( "unexpected http code: %d, URL: %s" , res . StatusCode , authReq . URL )
2016-07-20 09:46:01 +00:00
}
tokenBlob , err := ioutil . ReadAll ( res . Body )
if err != nil {
2017-02-03 19:15:09 +00:00
return nil , err
}
var token bearerToken
if err := json . Unmarshal ( tokenBlob , & token ) ; err != nil {
return nil , err
}
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 ( )
}
return & token , nil
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
func getAuth ( ctx * types . SystemContext , registry string ) ( string , string , error ) {
if ctx != nil && ctx . DockerAuthConfig != nil {
return ctx . DockerAuthConfig . Username , ctx . DockerAuthConfig . Password , nil
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
var dockerAuth dockerConfigFile
2016-07-20 09:46:01 +00:00
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
}
2016-11-22 19:32:10 +00:00
2016-07-20 09:46:01 +00:00
} else if os . IsNotExist ( err ) {
2016-11-22 19:32:10 +00:00
// try old config path
2016-07-20 09:46:01 +00:00
oldDockerCfgPath := filepath . Join ( getDefaultConfigDir ( dockerCfgObsolete ) )
if _ , err := os . Stat ( oldDockerCfgPath ) ; err != nil {
2016-11-22 19:32:10 +00:00
if os . IsNotExist ( err ) {
return "" , "" , nil
}
2016-10-17 13:53:40 +00:00
return "" , "" , errors . Wrap ( err , oldDockerCfgPath )
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
2016-07-20 09:46:01 +00:00
j , err := ioutil . ReadFile ( oldDockerCfgPath )
if err != nil {
return "" , "" , err
}
2016-11-22 19:32:10 +00:00
if err := json . Unmarshal ( j , & dockerAuth . AuthConfigs ) ; err != nil {
2016-07-20 09:46:01 +00:00
return "" , "" , err
}
2016-11-22 19:32:10 +00:00
} else if err != nil {
2016-10-17 13:53:40 +00:00
return "" , "" , errors . Wrap ( err , dockerCfgPath )
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
// 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
2016-07-20 09:46:01 +00:00
}
2017-02-01 00:45:59 +00:00
func ( c * dockerClient ) ping ( ) error {
ping := func ( scheme string ) error {
2016-07-20 09:46:01 +00:00
url := fmt . Sprintf ( baseURL , scheme , c . registry )
2016-11-22 19:32:10 +00:00
resp , err := c . makeRequestToResolvedURL ( "GET" , url , nil , nil , - 1 , true )
2016-07-20 09:46:01 +00:00
logrus . Debugf ( "Ping %s err %#v" , url , err )
if err != nil {
2017-02-01 00:45:59 +00:00
return err
2016-07-20 09:46:01 +00:00
}
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 {
2017-02-01 00:45:59 +00:00
return errors . Errorf ( "error pinging repository, response code %d" , resp . StatusCode )
2016-07-20 09:46:01 +00:00
}
2017-02-01 00:45:59 +00:00
c . challenges = parseAuthHeader ( resp . Header )
c . scheme = scheme
return nil
2016-07-20 09:46:01 +00:00
}
2017-02-01 00:45:59 +00:00
err := ping ( "https" )
2016-11-22 19:32:10 +00:00
if err != nil && c . ctx != nil && c . ctx . DockerInsecureSkipTLSVerify {
2017-02-01 00:45:59 +00:00
err = ping ( "http" )
2016-07-20 09:46:01 +00:00
}
2016-11-22 19:32:10 +00:00
if err != nil {
2016-10-17 13:53:40 +00:00
err = errors . Wrap ( err , "pinging docker registry returned" )
2016-11-22 19:32:10 +00:00
if c . ctx != nil && c . ctx . DockerDisableV1Ping {
2017-02-01 00:45:59 +00:00
return err
2016-11-22 19:32:10 +00:00
}
// 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
}
}
2017-02-01 00:45:59 +00:00
return err
2016-07-20 09:46:01 +00:00
}
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
}
2016-11-22 19:32:10 +00:00
// 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
}