4f0d95fa |
package registry // import "github.com/docker/docker/registry" |
752dd707 |
import ( |
ae3b59c1 |
// this is required for some certificates |
752dd707 |
_ "crypto/sha512"
"encoding/json"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings" |
f23c00d8 |
"sync" |
752dd707 |
|
91e197d6 |
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry" |
d453fe35 |
"github.com/docker/docker/errdefs" |
276c640b |
"github.com/docker/docker/pkg/ioutils" |
a6ac5495 |
"github.com/docker/docker/pkg/jsonmessage" |
1b67c38f |
"github.com/docker/docker/pkg/stringid" |
ebcb7d6b |
"github.com/pkg/errors" |
1009e6a4 |
"github.com/sirupsen/logrus" |
752dd707 |
)
|
4fcb9ac4 |
// A Session is used to communicate with a V1 registry |
752dd707 |
type Session struct { |
f2d481a2 |
indexEndpoint *V1Endpoint |
a01cc3ca |
client *http.Client
// TODO(tiborvass): remove authConfig |
5b321e32 |
authConfig *types.AuthConfig |
1b67c38f |
id string |
752dd707 |
}
|
73823e5e |
type authTransport struct {
http.RoundTripper |
5b321e32 |
*types.AuthConfig |
73823e5e |
alwaysSetBasicAuth bool
token []string
mu sync.Mutex // guards modReq
modReq map[*http.Request]*http.Request // original -> modified
}
// AuthTransport handles the auth layer when communicating with a v1 registry (private or official) |
a01cc3ca |
//
// For private v1 registries, set alwaysSetBasicAuth to true.
//
// For the official v1 registry, if there isn't already an Authorization header in the request,
// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
// requests.
//
// If the server sends a token without the client having requested it, it is ignored.
//
// This RoundTripper also has a CancelRequest method important for correct timeout handling. |
5b321e32 |
func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper { |
73823e5e |
if base == nil {
base = http.DefaultTransport
}
return &authTransport{
RoundTripper: base,
AuthConfig: authConfig,
alwaysSetBasicAuth: alwaysSetBasicAuth,
modReq: make(map[*http.Request]*http.Request),
} |
a01cc3ca |
} |
752dd707 |
|
276c640b |
// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
|
c1be45fa |
// RoundTrip changes an HTTP request's headers to add the necessary |
4fcb9ac4 |
// authentication-related headers |
73823e5e |
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { |
123a0582 |
// Authorization should not be set on 302 redirect for untrusted locations. |
4fcb9ac4 |
// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. |
123a0582 |
// As the authorization logic is currently implemented in RoundTrip, |
927b334e |
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
// This is safe as Docker doesn't set Referrer in other scenarios. |
123a0582 |
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
return tr.RoundTripper.RoundTrip(orig)
}
|
19515a7a |
req := cloneRequest(orig) |
73823e5e |
tr.mu.Lock()
tr.modReq[orig] = req
tr.mu.Unlock() |
a01cc3ca |
if tr.alwaysSetBasicAuth { |
b32c4cb4 |
if tr.AuthConfig == nil {
return nil, errors.New("unexpected error: empty auth config")
} |
a01cc3ca |
req.SetBasicAuth(tr.Username, tr.Password)
return tr.RoundTripper.RoundTrip(req) |
752dd707 |
}
|
a01cc3ca |
// Don't override
if req.Header.Get("Authorization") == "" { |
b32c4cb4 |
if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 { |
a01cc3ca |
req.SetBasicAuth(tr.Username, tr.Password) |
123a0582 |
} else if len(tr.token) > 0 { |
a01cc3ca |
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
}
}
resp, err := tr.RoundTripper.RoundTrip(req) |
752dd707 |
if err != nil { |
49fbb9c9 |
tr.mu.Lock() |
73823e5e |
delete(tr.modReq, orig) |
49fbb9c9 |
tr.mu.Unlock() |
752dd707 |
return nil, err
} |
fc29f7f7 |
if len(resp.Header["X-Docker-Token"]) > 0 { |
a01cc3ca |
tr.token = resp.Header["X-Docker-Token"]
} |
276c640b |
resp.Body = &ioutils.OnEOFReader{ |
73823e5e |
Rc: resp.Body, |
9d98c288 |
Fn: func() {
tr.mu.Lock()
delete(tr.modReq, orig)
tr.mu.Unlock()
}, |
73823e5e |
} |
a01cc3ca |
return resp, nil
}
|
73823e5e |
// CancelRequest cancels an in-flight request by closing its connection.
func (tr *authTransport) CancelRequest(req *http.Request) {
type canceler interface {
CancelRequest(*http.Request)
}
if cr, ok := tr.RoundTripper.(canceler); ok {
tr.mu.Lock()
modReq := tr.modReq[req]
delete(tr.modReq, req)
tr.mu.Unlock()
cr.CancelRequest(modReq)
}
}
|
19d48f0b |
func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { |
a01cc3ca |
var alwaysSetBasicAuth bool |
752dd707 |
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers |
a01cc3ca |
// alongside all our requests. |
f2d481a2 |
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { |
a01cc3ca |
info, err := endpoint.Ping() |
752dd707 |
if err != nil { |
19d48f0b |
return err |
752dd707 |
} |
a01cc3ca |
if info.Standalone && authConfig != nil {
logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
alwaysSetBasicAuth = true |
752dd707 |
}
}
|
c2315102 |
// Annotate the transport unconditionally so that v2 can
// properly fallback on v1 when an image is not found.
client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) |
752dd707 |
|
a01cc3ca |
jar, err := cookiejar.New(nil)
if err != nil { |
19d48f0b |
return errors.New("cookiejar.New is not supposed to return an error") |
a01cc3ca |
}
client.Jar = jar
|
19d48f0b |
return nil
}
func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session {
return &Session{
authConfig: authConfig,
client: client,
indexEndpoint: endpoint,
id: stringid.GenerateRandomID(),
}
}
// NewSession creates a new session
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) {
if err := authorizeClient(client, authConfig, endpoint); err != nil {
return nil, err
}
return newSession(client, authConfig, endpoint), nil |
752dd707 |
}
|
4fcb9ac4 |
// SearchRepositories performs a search against the remote repository |
92f10fe2 |
func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
if limit < 1 || limit > 100 { |
87a12421 |
return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit)) |
92f10fe2 |
} |
6f4d8470 |
logrus.Debugf("Index server: %s", r.indexEndpoint) |
92f10fe2 |
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) |
5a170484 |
|
441b031b |
req, err := http.NewRequest(http.MethodGet, u, nil) |
5a170484 |
if err != nil { |
87a12421 |
return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request") |
5a170484 |
}
// Have the AuthTransport send authentication, when logged in.
req.Header.Set("X-Docker-Token", "true")
res, err := r.client.Do(req) |
752dd707 |
if err != nil { |
87a12421 |
return nil, errdefs.System(err) |
752dd707 |
}
defer res.Body.Close() |
63e62d13 |
if res.StatusCode != http.StatusOK { |
3f7c62f6 |
return nil, &jsonmessage.JSONError{
Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode),
Code: res.StatusCode,
} |
752dd707 |
} |
c4472b38 |
result := new(registrytypes.SearchResults) |
ebcb7d6b |
return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results") |
752dd707 |
} |