github.com/pwn-term/docker@v0.0.0-20210616085119-6e977cce2565/moby/registry/session.go (about) 1 package registry // import "github.com/docker/docker/registry" 2 3 import ( 4 // this is required for some certificates 5 _ "crypto/sha512" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "net/http/cookiejar" 10 "net/url" 11 "strings" 12 "sync" 13 14 "github.com/docker/docker/api/types" 15 registrytypes "github.com/docker/docker/api/types/registry" 16 "github.com/docker/docker/errdefs" 17 "github.com/docker/docker/pkg/ioutils" 18 "github.com/docker/docker/pkg/jsonmessage" 19 "github.com/docker/docker/pkg/stringid" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 ) 23 24 // A Session is used to communicate with a V1 registry 25 type Session struct { 26 indexEndpoint *V1Endpoint 27 client *http.Client 28 // TODO(tiborvass): remove authConfig 29 authConfig *types.AuthConfig 30 id string 31 } 32 33 type authTransport struct { 34 http.RoundTripper 35 *types.AuthConfig 36 37 alwaysSetBasicAuth bool 38 token []string 39 40 mu sync.Mutex // guards modReq 41 modReq map[*http.Request]*http.Request // original -> modified 42 } 43 44 // AuthTransport handles the auth layer when communicating with a v1 registry (private or official) 45 // 46 // For private v1 registries, set alwaysSetBasicAuth to true. 47 // 48 // For the official v1 registry, if there isn't already an Authorization header in the request, 49 // but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. 50 // After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing 51 // a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent 52 // requests. 53 // 54 // If the server sends a token without the client having requested it, it is ignored. 55 // 56 // This RoundTripper also has a CancelRequest method important for correct timeout handling. 57 func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper { 58 if base == nil { 59 base = http.DefaultTransport 60 } 61 return &authTransport{ 62 RoundTripper: base, 63 AuthConfig: authConfig, 64 alwaysSetBasicAuth: alwaysSetBasicAuth, 65 modReq: make(map[*http.Request]*http.Request), 66 } 67 } 68 69 // cloneRequest returns a clone of the provided *http.Request. 70 // The clone is a shallow copy of the struct and its Header map. 71 func cloneRequest(r *http.Request) *http.Request { 72 // shallow copy of the struct 73 r2 := new(http.Request) 74 *r2 = *r 75 // deep copy of the Header 76 r2.Header = make(http.Header, len(r.Header)) 77 for k, s := range r.Header { 78 r2.Header[k] = append([]string(nil), s...) 79 } 80 81 return r2 82 } 83 84 // RoundTrip changes an HTTP request's headers to add the necessary 85 // authentication-related headers 86 func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { 87 // Authorization should not be set on 302 redirect for untrusted locations. 88 // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. 89 // As the authorization logic is currently implemented in RoundTrip, 90 // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. 91 // This is safe as Docker doesn't set Referrer in other scenarios. 92 if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { 93 return tr.RoundTripper.RoundTrip(orig) 94 } 95 96 req := cloneRequest(orig) 97 tr.mu.Lock() 98 tr.modReq[orig] = req 99 tr.mu.Unlock() 100 101 if tr.alwaysSetBasicAuth { 102 if tr.AuthConfig == nil { 103 return nil, errors.New("unexpected error: empty auth config") 104 } 105 req.SetBasicAuth(tr.Username, tr.Password) 106 return tr.RoundTripper.RoundTrip(req) 107 } 108 109 // Don't override 110 if req.Header.Get("Authorization") == "" { 111 if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 { 112 req.SetBasicAuth(tr.Username, tr.Password) 113 } else if len(tr.token) > 0 { 114 req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) 115 } 116 } 117 resp, err := tr.RoundTripper.RoundTrip(req) 118 if err != nil { 119 tr.mu.Lock() 120 delete(tr.modReq, orig) 121 tr.mu.Unlock() 122 return nil, err 123 } 124 if len(resp.Header["X-Docker-Token"]) > 0 { 125 tr.token = resp.Header["X-Docker-Token"] 126 } 127 resp.Body = &ioutils.OnEOFReader{ 128 Rc: resp.Body, 129 Fn: func() { 130 tr.mu.Lock() 131 delete(tr.modReq, orig) 132 tr.mu.Unlock() 133 }, 134 } 135 return resp, nil 136 } 137 138 // CancelRequest cancels an in-flight request by closing its connection. 139 func (tr *authTransport) CancelRequest(req *http.Request) { 140 type canceler interface { 141 CancelRequest(*http.Request) 142 } 143 if cr, ok := tr.RoundTripper.(canceler); ok { 144 tr.mu.Lock() 145 modReq := tr.modReq[req] 146 delete(tr.modReq, req) 147 tr.mu.Unlock() 148 cr.CancelRequest(modReq) 149 } 150 } 151 152 func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { 153 var alwaysSetBasicAuth bool 154 155 // If we're working with a standalone private registry over HTTPS, send Basic Auth headers 156 // alongside all our requests. 157 if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { 158 info, err := endpoint.Ping() 159 if err != nil { 160 return err 161 } 162 if info.Standalone && authConfig != nil { 163 logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String()) 164 alwaysSetBasicAuth = true 165 } 166 } 167 168 // Annotate the transport unconditionally so that v2 can 169 // properly fallback on v1 when an image is not found. 170 client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) 171 172 jar, err := cookiejar.New(nil) 173 if err != nil { 174 return errors.New("cookiejar.New is not supposed to return an error") 175 } 176 client.Jar = jar 177 178 return nil 179 } 180 181 func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session { 182 return &Session{ 183 authConfig: authConfig, 184 client: client, 185 indexEndpoint: endpoint, 186 id: stringid.GenerateRandomID(), 187 } 188 } 189 190 // NewSession creates a new session 191 // TODO(tiborvass): remove authConfig param once registry client v2 is vendored 192 func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) { 193 if err := authorizeClient(client, authConfig, endpoint); err != nil { 194 return nil, err 195 } 196 197 return newSession(client, authConfig, endpoint), nil 198 } 199 200 // SearchRepositories performs a search against the remote repository 201 func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) { 202 if limit < 1 || limit > 100 { 203 return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit)) 204 } 205 logrus.Debugf("Index server: %s", r.indexEndpoint) 206 u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) 207 208 req, err := http.NewRequest(http.MethodGet, u, nil) 209 if err != nil { 210 return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request") 211 } 212 // Have the AuthTransport send authentication, when logged in. 213 req.Header.Set("X-Docker-Token", "true") 214 res, err := r.client.Do(req) 215 if err != nil { 216 return nil, errdefs.System(err) 217 } 218 defer res.Body.Close() 219 if res.StatusCode != http.StatusOK { 220 return nil, &jsonmessage.JSONError{ 221 Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode), 222 Code: res.StatusCode, 223 } 224 } 225 result := new(registrytypes.SearchResults) 226 return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results") 227 }