Merge pull request #739 from stevvooe/etags-must-be-quoted

Etags must be quoted according to http spec
pull/750/head
Stephen Day 2015-07-24 15:08:27 -07:00
commit b49d77a42f
5 changed files with 16 additions and 14 deletions

View File

@ -254,13 +254,14 @@ func (ms *manifests) Get(dgst digest.Digest) (*manifest.SignedManifest, error) {
return ms.GetByTag(dgst.String()) return ms.GetByTag(dgst.String())
} }
// AddEtagToTag allows a client to supply an eTag to GetByTag which will // AddEtagToTag allows a client to supply an eTag to GetByTag which will be
// be used for a conditional HTTP request. If the eTag matches, a nil // used for a conditional HTTP request. If the eTag matches, a nil manifest
// manifest and nil error will be returned. // and nil error will be returned. etag is automatically quoted when added to
func AddEtagToTag(tagName, dgst string) distribution.ManifestServiceOption { // this map.
func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
return func(ms distribution.ManifestService) error { return func(ms distribution.ManifestService) error {
if ms, ok := ms.(*manifests); ok { if ms, ok := ms.(*manifests); ok {
ms.etags[tagName] = dgst ms.etags[tag] = fmt.Sprintf(`"%s"`, etag)
return nil return nil
} }
return fmt.Errorf("etag options is a client-only option") return fmt.Errorf("etag options is a client-only option")

View File

@ -463,7 +463,7 @@ func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil
Method: "GET", Method: "GET",
Route: "/v2/" + repo + "/manifests/" + reference, Route: "/v2/" + repo + "/manifests/" + reference,
Headers: http.Header(map[string][]string{ Headers: http.Header(map[string][]string{
"Etag": {dgst}, "Etag": {fmt.Sprintf(`"%s"`, dgst)},
}), }),
} }

View File

@ -488,7 +488,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)}, "Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{canonicalDigest.String()}, "Docker-Content-Digest": []string{canonicalDigest.String()},
"ETag": []string{canonicalDigest.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, canonicalDigest)},
"Cache-Control": []string{"max-age=31536000"}, "Cache-Control": []string{"max-age=31536000"},
}) })
@ -499,6 +499,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
t.Fatalf("Error constructing request: %s", err) t.Fatalf("Error constructing request: %s", err)
} }
req.Header.Set("If-None-Match", etag) req.Header.Set("If-None-Match", etag)
resp, err = http.DefaultClient.Do(req) resp, err = http.DefaultClient.Do(req)
if err != nil { if err != nil {
t.Fatalf("Error constructing request: %s", err) t.Fatalf("Error constructing request: %s", err)
@ -835,7 +836,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()}, "Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
}) })
var fetchedManifest manifest.SignedManifest var fetchedManifest manifest.SignedManifest
@ -857,7 +858,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()}, "Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
}) })
var fetchedManifestByDigest manifest.SignedManifest var fetchedManifestByDigest manifest.SignedManifest
@ -1301,12 +1302,12 @@ func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) {
for _, v := range vs { for _, v := range vs {
if v == "*" { if v == "*" {
// Just ensure there is some value. // Just ensure there is some value.
if len(resp.Header[k]) > 0 { if len(resp.Header[http.CanonicalHeaderKey(k)]) > 0 {
continue continue
} }
} }
for _, hv := range resp.Header[k] { for _, hv := range resp.Header[http.CanonicalHeaderKey(k)] {
if hv != v { if hv != v {
t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v) t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v)
} }

View File

@ -90,13 +90,13 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw))) w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw)))
w.Header().Set("Docker-Content-Digest", imh.Digest.String()) w.Header().Set("Docker-Content-Digest", imh.Digest.String())
w.Header().Set("Etag", imh.Digest.String()) w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
w.Write(sm.Raw) w.Write(sm.Raw)
} }
func etagMatch(r *http.Request, etag string) bool { func etagMatch(r *http.Request, etag string) bool {
for _, headerVal := range r.Header["If-None-Match"] { for _, headerVal := range r.Header["If-None-Match"] {
if headerVal == etag { if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
return true return true
} }
} }

View File

@ -47,7 +47,7 @@ func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *h
} }
defer br.Close() defer br.Close()
w.Header().Set("ETag", desc.Digest.String()) // If-None-Match handled by ServeContent w.Header().Set("ETag", fmt.Sprintf(`"%s"`, desc.Digest)) // If-None-Match handled by ServeContent
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds())) w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds()))
if w.Header().Get("Docker-Content-Digest") == "" { if w.Header().Get("Docker-Content-Digest") == "" {