1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
21 "git.arvados.org/arvados.git/sdk/go/arvados"
22 "git.arvados.org/arvados.git/sdk/go/arvadostest"
23 "git.arvados.org/arvados.git/sdk/go/httpserver"
24 "github.com/prometheus/client_golang/prometheus"
28 // routerSuite tests that the router correctly translates HTTP
29 // requests to the appropriate keepstore functionality, and translates
30 // the results to HTTP responses.
31 type routerSuite struct {
32 cluster *arvados.Cluster
35 var _ = Suite(&routerSuite{})
37 func testRouter(t TB, cluster *arvados.Cluster, reg *prometheus.Registry) (*router, context.CancelFunc) {
39 reg = prometheus.NewRegistry()
41 ctx, cancel := context.WithCancel(context.Background())
42 ks, kcancel := testKeepstore(t, cluster, reg)
47 puller := newPuller(ctx, ks, reg)
48 trasher := newTrasher(ctx, ks, reg)
49 return newRouter(ks, puller, trasher).(*router), cancel
52 func (s *routerSuite) SetUpTest(c *C) {
53 s.cluster = testCluster(c)
54 s.cluster.Volumes = map[string]arvados.Volume{
55 "zzzzz-nyw5e-000000000000000": {Replication: 1, Driver: "stub", StorageClasses: map[string]bool{"testclass1": true}},
56 "zzzzz-nyw5e-111111111111111": {Replication: 1, Driver: "stub", StorageClasses: map[string]bool{"testclass2": true}},
58 s.cluster.StorageClasses = map[string]arvados.StorageClassConfig{
59 "testclass1": arvados.StorageClassConfig{
62 "testclass2": arvados.StorageClassConfig{
68 func (s *routerSuite) TestBlockRead_Token(c *C) {
69 router, cancel := testRouter(c, s.cluster, nil)
72 err := router.keepstore.mountsW[0].BlockWrite(context.Background(), fooHash, []byte("foo"))
74 locSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, fooHash+"+3")
75 c.Assert(locSigned, Not(Equals), fooHash+"+3")
78 resp := call(router, "GET", "http://example/"+locSigned, "", nil, nil)
79 c.Check(resp.Code, Equals, http.StatusUnauthorized)
80 c.Check(resp.Body.String(), Matches, "no token provided in Authorization header\n")
81 checkCORSHeaders(c, resp.Header())
83 // Different token => invalid signature
84 resp = call(router, "GET", "http://example/"+locSigned, "badtoken", nil, nil)
85 c.Check(resp.Code, Equals, http.StatusBadRequest)
86 c.Check(resp.Body.String(), Equals, "invalid signature\n")
87 checkCORSHeaders(c, resp.Header())
90 resp = call(router, "GET", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
91 c.Check(resp.Code, Equals, http.StatusOK)
92 c.Check(resp.Body.String(), Equals, "foo")
93 checkCORSHeaders(c, resp.Header())
96 resp = call(router, "HEAD", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
97 c.Check(resp.Code, Equals, http.StatusOK)
98 c.Check(resp.Result().ContentLength, Equals, int64(3))
99 c.Check(resp.Body.String(), Equals, "")
100 checkCORSHeaders(c, resp.Header())
103 // Previous versions responded to "GET //locator" with a 301 redirect
104 // to "/locator". To preserve compatibility with
105 // clients/configurations that depended on this, we now accept "GET
106 // //locator" as a synonym of "GET /locator", i.e., we respond with
107 // the data instead of a redirect.
109 // More generally, requests with double slashes are not accepted (see
111 func (s *routerSuite) TestBlockRead_DoubleSlash(c *C) {
112 router, cancel := testRouter(c, s.cluster, nil)
115 err := router.keepstore.mountsW[0].BlockWrite(context.Background(), fooHash, []byte("foo"))
117 locSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, fooHash+"+3")
118 c.Assert(locSigned, Not(Equals), fooHash+"+3")
120 resp := call(router, "GET", "http://example//"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
121 c.Check(resp.Code, Equals, http.StatusOK)
122 c.Check(resp.Body.String(), Equals, "foo")
123 checkCORSHeaders(c, resp.Header())
126 resp = call(router, "HEAD", "http://example//"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
127 c.Check(resp.Code, Equals, http.StatusOK)
128 c.Check(resp.Result().ContentLength, Equals, int64(3))
129 c.Check(resp.Body.String(), Equals, "")
130 checkCORSHeaders(c, resp.Header())
133 // As a special case we allow HEAD requests that only provide a hash
134 // without a size hint. This accommodates uses of keep-block-check
135 // where it's inconvenient to attach size hints to known hashes.
137 // GET requests must provide a size hint -- otherwise we can't
138 // propagate a checksum mismatch error.
139 func (s *routerSuite) TestBlockRead_NoSizeHint(c *C) {
140 s.cluster.Collections.BlobSigning = true
141 router, cancel := testRouter(c, s.cluster, nil)
143 err := router.keepstore.mountsW[0].BlockWrite(context.Background(), fooHash, []byte("foo"))
147 hashSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, fooHash)
148 resp := call(router, "GET", "http://example/"+hashSigned, arvadostest.ActiveTokenV2, nil, nil)
149 c.Check(resp.Code, Equals, http.StatusMethodNotAllowed)
151 resp = call(router, "HEAD", "http://example/"+fooHash, "", nil, nil)
152 c.Check(resp.Code, Equals, http.StatusUnauthorized)
153 resp = call(router, "HEAD", "http://example/"+fooHash+"+3", "", nil, nil)
154 c.Check(resp.Code, Equals, http.StatusUnauthorized)
156 s.cluster.Collections.BlobSigning = false
157 router, cancel = testRouter(c, s.cluster, nil)
159 err = router.keepstore.mountsW[0].BlockWrite(context.Background(), fooHash, []byte("foo"))
162 resp = call(router, "GET", "http://example/"+fooHash, "", nil, nil)
163 c.Check(resp.Code, Equals, http.StatusMethodNotAllowed)
165 resp = call(router, "HEAD", "http://example/"+fooHash, "", nil, nil)
166 c.Check(resp.Code, Equals, http.StatusOK)
167 c.Check(resp.Body.String(), Equals, "")
168 c.Check(resp.Result().ContentLength, Equals, int64(3))
169 c.Check(resp.Header().Get("Content-Length"), Equals, "3")
172 // By the time we discover the checksum mismatch, it's too late to
173 // change the response code, but the expected block size is given in
174 // the Content-Length response header, so a generic http client can
175 // detect the problem.
176 func (s *routerSuite) TestBlockRead_ChecksumMismatch(c *C) {
177 router, cancel := testRouter(c, s.cluster, nil)
180 gooddata := make([]byte, 10_000_000)
182 hash := fmt.Sprintf("%x", md5.Sum(gooddata))
183 locSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, fmt.Sprintf("%s+%d", hash, len(gooddata)))
185 for _, baddata := range [][]byte{
187 make([]byte, len(gooddata)),
188 make([]byte, len(gooddata)-1),
189 make([]byte, len(gooddata)+1),
190 make([]byte, len(gooddata)*2),
192 c.Logf("=== baddata len %d", len(baddata))
193 err := router.keepstore.mountsW[0].BlockWrite(context.Background(), hash, baddata)
196 resp := call(router, "GET", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
197 if !c.Check(resp.Code, Equals, http.StatusOK) {
198 c.Logf("resp.Body: %s", resp.Body.String())
200 c.Check(resp.Body.Len(), Not(Equals), len(gooddata))
201 c.Check(resp.Result().ContentLength, Equals, int64(len(gooddata)))
202 checkCORSHeaders(c, resp.Header())
204 resp = call(router, "HEAD", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
205 c.Check(resp.Code, Equals, http.StatusBadGateway)
206 checkCORSHeaders(c, resp.Header())
208 hashSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, hash)
209 resp = call(router, "HEAD", "http://example/"+hashSigned, arvadostest.ActiveTokenV2, nil, nil)
210 c.Check(resp.Code, Equals, http.StatusBadGateway)
211 checkCORSHeaders(c, resp.Header())
215 func (s *routerSuite) TestBlockWrite(c *C) {
216 router, cancel := testRouter(c, s.cluster, nil)
219 resp := call(router, "PUT", "http://example/"+fooHash, arvadostest.ActiveTokenV2, []byte("foo"), nil)
220 c.Check(resp.Code, Equals, http.StatusOK)
221 checkCORSHeaders(c, resp.Header())
222 locator := strings.TrimSpace(resp.Body.String())
224 resp = call(router, "GET", "http://example/"+locator, arvadostest.ActiveTokenV2, nil, nil)
225 c.Check(resp.Code, Equals, http.StatusOK)
226 c.Check(resp.Body.String(), Equals, "foo")
229 func (s *routerSuite) TestBlockWrite_Headers(c *C) {
230 router, cancel := testRouter(c, s.cluster, nil)
233 resp := call(router, "PUT", "http://example/"+fooHash, arvadostest.ActiveTokenV2, []byte("foo"), http.Header{"X-Keep-Desired-Replicas": []string{"2"}})
234 c.Check(resp.Code, Equals, http.StatusOK)
235 c.Check(resp.Header().Get("X-Keep-Replicas-Stored"), Equals, "1")
236 c.Check(sortCommaSeparated(resp.Header().Get("X-Keep-Storage-Classes-Confirmed")), Equals, "testclass1=1")
238 resp = call(router, "PUT", "http://example/"+fooHash, arvadostest.ActiveTokenV2, []byte("foo"), http.Header{"X-Keep-Storage-Classes": []string{"testclass1"}})
239 c.Check(resp.Code, Equals, http.StatusOK)
240 c.Check(resp.Header().Get("X-Keep-Replicas-Stored"), Equals, "1")
241 c.Check(resp.Header().Get("X-Keep-Storage-Classes-Confirmed"), Equals, "testclass1=1")
243 resp = call(router, "PUT", "http://example/"+fooHash, arvadostest.ActiveTokenV2, []byte("foo"), http.Header{"X-Keep-Storage-Classes": []string{" , testclass2 , "}})
244 c.Check(resp.Code, Equals, http.StatusOK)
245 c.Check(resp.Header().Get("X-Keep-Replicas-Stored"), Equals, "1")
246 c.Check(resp.Header().Get("X-Keep-Storage-Classes-Confirmed"), Equals, "testclass2=1")
248 resp = call(router, "PUT", "http://example/"+fooHash, arvadostest.ActiveTokenV2, []byte("foo"), http.Header{"X-Keep-Storage-Classes": []string{"testclass1, testclass2"}})
249 c.Check(resp.Code, Equals, http.StatusOK)
250 c.Check(resp.Header().Get("X-Keep-Replicas-Stored"), Equals, "2")
251 confirmed := strings.Split(resp.Header().Get("X-Keep-Storage-Classes-Confirmed"), ", ")
252 sort.Strings(confirmed)
253 c.Check(confirmed, DeepEquals, []string{"testclass1=1", "testclass2=1"})
256 func sortCommaSeparated(s string) string {
257 slice := strings.Split(s, ", ")
259 return strings.Join(slice, ", ")
262 func (s *routerSuite) TestBlockTouch(c *C) {
263 router, cancel := testRouter(c, s.cluster, nil)
266 resp := call(router, "TOUCH", "http://example/"+fooHash+"+3", s.cluster.SystemRootToken, nil, nil)
267 c.Check(resp.Code, Equals, http.StatusNotFound)
269 vol0 := router.keepstore.mountsW[0].volume.(*stubVolume)
270 err := vol0.BlockWrite(context.Background(), fooHash, []byte("foo"))
272 vol1 := router.keepstore.mountsW[1].volume.(*stubVolume)
273 err = vol1.BlockWrite(context.Background(), fooHash, []byte("foo"))
277 resp = call(router, "TOUCH", "http://example/"+fooHash+"+3", s.cluster.SystemRootToken, nil, nil)
278 c.Check(resp.Code, Equals, http.StatusOK)
281 // Unauthorized request is a no-op
282 resp = call(router, "TOUCH", "http://example/"+fooHash+"+3", arvadostest.ActiveTokenV2, nil, nil)
283 c.Check(resp.Code, Equals, http.StatusForbidden)
285 // Volume 0 mtime should be updated
286 t, err := vol0.Mtime(fooHash)
288 c.Check(t.After(t1), Equals, true)
289 c.Check(t.Before(t2), Equals, true)
291 // Volume 1 mtime should not be updated
292 t, err = vol1.Mtime(fooHash)
294 c.Check(t.Before(t1), Equals, true)
296 err = vol0.BlockTrash(fooHash)
298 err = vol1.BlockTrash(fooHash)
300 resp = call(router, "TOUCH", "http://example/"+fooHash+"+3", s.cluster.SystemRootToken, nil, nil)
301 c.Check(resp.Code, Equals, http.StatusNotFound)
304 func (s *routerSuite) TestBlockTrash(c *C) {
305 router, cancel := testRouter(c, s.cluster, nil)
308 vol0 := router.keepstore.mountsW[0].volume.(*stubVolume)
309 err := vol0.BlockWrite(context.Background(), fooHash, []byte("foo"))
311 err = vol0.blockTouchWithTime(fooHash, time.Now().Add(-s.cluster.Collections.BlobSigningTTL.Duration()))
313 resp := call(router, "DELETE", "http://example/"+fooHash+"+3", s.cluster.SystemRootToken, nil, nil)
314 c.Check(resp.Code, Equals, http.StatusOK)
315 c.Check(vol0.stubLog.String(), Matches, `(?ms).* trash .*`)
316 err = vol0.BlockRead(context.Background(), fooHash, brdiscard)
317 c.Assert(err, Equals, os.ErrNotExist)
320 func (s *routerSuite) TestBlockUntrash(c *C) {
321 router, cancel := testRouter(c, s.cluster, nil)
324 vol0 := router.keepstore.mountsW[0].volume.(*stubVolume)
325 err := vol0.BlockWrite(context.Background(), fooHash, []byte("foo"))
327 err = vol0.BlockTrash(fooHash)
329 err = vol0.BlockRead(context.Background(), fooHash, brdiscard)
330 c.Assert(err, Equals, os.ErrNotExist)
331 resp := call(router, "PUT", "http://example/untrash/"+fooHash+"+3", s.cluster.SystemRootToken, nil, nil)
332 c.Check(resp.Code, Equals, http.StatusOK)
333 c.Check(vol0.stubLog.String(), Matches, `(?ms).* untrash .*`)
334 err = vol0.BlockRead(context.Background(), fooHash, brdiscard)
338 func (s *routerSuite) TestBadRequest(c *C) {
339 router, cancel := testRouter(c, s.cluster, nil)
342 for _, trial := range []string{
345 "GET /aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdefg",
347 "GET /mounts/blocks/123",
350 "GET /debug.json", // old endpoint, no longer exists
351 "GET /status.json", // old endpoint, no longer exists
354 "PUT //aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
356 "POST /aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
361 c.Logf("=== %s", trial)
362 methodpath := strings.Split(trial, " ")
363 req := httptest.NewRequest(methodpath[0], "http://example"+methodpath[1], nil)
364 resp := httptest.NewRecorder()
365 router.ServeHTTP(resp, req)
366 c.Check(resp.Code, Equals, http.StatusBadRequest)
370 func (s *routerSuite) TestRequireAdminMgtToken(c *C) {
371 router, cancel := testRouter(c, s.cluster, nil)
374 for _, token := range []string{"badtoken", ""} {
375 for _, trial := range []string{
381 "PUT /untrash/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
383 c.Logf("=== %s", trial)
384 methodpath := strings.Split(trial, " ")
385 req := httptest.NewRequest(methodpath[0], "http://example"+methodpath[1], nil)
387 req.Header.Set("Authorization", "Bearer "+token)
389 resp := httptest.NewRecorder()
390 router.ServeHTTP(resp, req)
392 c.Check(resp.Code, Equals, http.StatusUnauthorized)
394 c.Check(resp.Code, Equals, http.StatusForbidden)
398 req := httptest.NewRequest("TOUCH", "http://example/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil)
399 resp := httptest.NewRecorder()
400 router.ServeHTTP(resp, req)
401 c.Check(resp.Code, Equals, http.StatusUnauthorized)
404 func (s *routerSuite) TestVolumeErrorStatusCode(c *C) {
405 router, cancel := testRouter(c, s.cluster, nil)
407 router.keepstore.mountsW[0].volume.(*stubVolume).blockRead = func(_ context.Context, hash string, w io.WriterAt) error {
408 return httpserver.ErrorWithStatus(errors.New("test error"), http.StatusBadGateway)
411 // To test whether we fall back to volume 1 after volume 0
412 // returns an error, we need to use a block whose rendezvous
413 // order has volume 0 first. Luckily "bar" is such a block.
414 c.Assert(router.keepstore.rendezvous(barHash, router.keepstore.mountsR)[0].UUID, DeepEquals, router.keepstore.mountsR[0].UUID)
416 locSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, barHash+"+3")
418 // Volume 0 fails with an error that specifies an HTTP status
419 // code, so that code should be propagated to caller.
420 resp := call(router, "GET", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
421 c.Check(resp.Code, Equals, http.StatusBadGateway)
422 c.Check(resp.Body.String(), Equals, "test error\n")
424 router.keepstore.mountsW[0].volume.(*stubVolume).blockRead = func(_ context.Context, hash string, w io.WriterAt) error {
425 return errors.New("no http status provided")
427 resp = call(router, "GET", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
428 c.Check(resp.Code, Equals, http.StatusInternalServerError)
429 c.Check(resp.Body.String(), Equals, "no http status provided\n")
431 c.Assert(router.keepstore.mountsW[1].volume.BlockWrite(context.Background(), barHash, []byte("bar")), IsNil)
433 // If the requested block is available on the second volume,
434 // it doesn't matter that the first volume failed.
435 resp = call(router, "GET", "http://example/"+locSigned, arvadostest.ActiveTokenV2, nil, nil)
436 c.Check(resp.Code, Equals, http.StatusOK)
437 c.Check(resp.Body.String(), Equals, "bar")
440 func (s *routerSuite) TestIndex(c *C) {
441 router, cancel := testRouter(c, s.cluster, nil)
444 resp := call(router, "GET", "http://example/index", s.cluster.SystemRootToken, nil, nil)
445 c.Check(resp.Code, Equals, http.StatusOK)
446 c.Check(resp.Body.String(), Equals, "\n")
448 resp = call(router, "GET", "http://example/index?prefix=fff", s.cluster.SystemRootToken, nil, nil)
449 c.Check(resp.Code, Equals, http.StatusOK)
450 c.Check(resp.Body.String(), Equals, "\n")
452 t0 := time.Now().Add(-time.Hour)
453 vol0 := router.keepstore.mounts["zzzzz-nyw5e-000000000000000"].volume.(*stubVolume)
454 err := vol0.BlockWrite(context.Background(), fooHash, []byte("foo"))
456 err = vol0.blockTouchWithTime(fooHash, t0)
458 err = vol0.BlockWrite(context.Background(), barHash, []byte("bar"))
460 err = vol0.blockTouchWithTime(barHash, t0)
462 t1 := time.Now().Add(-time.Minute)
463 vol1 := router.keepstore.mounts["zzzzz-nyw5e-111111111111111"].volume.(*stubVolume)
464 err = vol1.BlockWrite(context.Background(), barHash, []byte("bar"))
466 err = vol1.blockTouchWithTime(barHash, t1)
469 for _, path := range []string{
472 "/index/?prefix=acb",
473 "/mounts/zzzzz-nyw5e-000000000000000/blocks?prefix=acb",
474 "/mounts/zzzzz-nyw5e-000000000000000/blocks/?prefix=acb",
475 "/mounts/zzzzz-nyw5e-000000000000000/blocks/acb",
477 c.Logf("=== %s", path)
478 resp = call(router, "GET", "http://example"+path, s.cluster.SystemRootToken, nil, nil)
479 c.Check(resp.Code, Equals, http.StatusOK)
480 c.Check(resp.Body.String(), Equals, fooHash+"+3 "+fmt.Sprintf("%d", t0.UnixNano())+"\n\n")
483 for _, path := range []string{
488 c.Logf("=== %s", path)
489 resp = call(router, "GET", "http://example"+path, s.cluster.SystemRootToken, nil, nil)
490 c.Check(resp.Code, Equals, http.StatusOK)
491 c.Check(resp.Body.String(), Equals, ""+
492 barHash+"+3 "+fmt.Sprintf("%d", t0.UnixNano())+"\n"+
493 barHash+"+3 "+fmt.Sprintf("%d", t1.UnixNano())+"\n\n")
496 for _, path := range []string{
497 "/mounts/zzzzz-nyw5e-111111111111111/blocks",
498 "/mounts/zzzzz-nyw5e-111111111111111/blocks/",
499 "/mounts/zzzzz-nyw5e-111111111111111/blocks?prefix=37",
500 "/mounts/zzzzz-nyw5e-111111111111111/blocks/?prefix=37",
501 "/mounts/zzzzz-nyw5e-111111111111111/blocks/37",
503 c.Logf("=== %s", path)
504 resp = call(router, "GET", "http://example"+path, s.cluster.SystemRootToken, nil, nil)
505 c.Check(resp.Code, Equals, http.StatusOK)
506 c.Check(resp.Body.String(), Equals, barHash+"+3 "+fmt.Sprintf("%d", t1.UnixNano())+"\n\n")
509 for _, path := range []string{
515 c.Logf("=== %s", path)
516 resp = call(router, "GET", "http://example"+path, s.cluster.SystemRootToken, nil, nil)
517 c.Check(resp.Code, Equals, http.StatusOK)
518 c.Check(strings.Split(resp.Body.String(), "\n"), HasLen, 5)
522 // Check that the context passed to a volume method gets cancelled
523 // when the http client hangs up.
524 func (s *routerSuite) TestCancelOnDisconnect(c *C) {
525 router, cancel := testRouter(c, s.cluster, nil)
528 unblock := make(chan struct{})
529 router.keepstore.mountsW[0].volume.(*stubVolume).blockRead = func(ctx context.Context, hash string, w io.WriterAt) error {
531 c.Check(ctx.Err(), NotNil)
535 time.Sleep(time.Second / 10)
539 locSigned := router.keepstore.signLocator(arvadostest.ActiveTokenV2, fooHash+"+3")
540 ctx, cancel := context.WithCancel(context.Background())
542 req, err := http.NewRequestWithContext(ctx, "GET", "http://example/"+locSigned, nil)
544 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
545 resp := httptest.NewRecorder()
546 router.ServeHTTP(resp, req)
547 c.Check(resp.Code, Equals, 499)
550 func (s *routerSuite) TestCORSPreflight(c *C) {
551 router, cancel := testRouter(c, s.cluster, nil)
554 for _, path := range []string{"/", "/whatever", "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+123"} {
555 c.Logf("=== %s", path)
556 resp := call(router, http.MethodOptions, "http://example"+path, arvadostest.ActiveTokenV2, nil, nil)
557 c.Check(resp.Code, Equals, http.StatusOK)
558 c.Check(resp.Body.String(), Equals, "")
559 checkCORSHeaders(c, resp.Header())
563 func call(handler http.Handler, method, path, tok string, body []byte, hdr http.Header) *httptest.ResponseRecorder {
564 resp := httptest.NewRecorder()
565 req, err := http.NewRequest(method, path, bytes.NewReader(body))
570 req.Header.Set(k, hdr.Get(k))
573 req.Header.Set("Authorization", "Bearer "+tok)
575 handler.ServeHTTP(resp, req)
579 func checkCORSHeaders(c *C, h http.Header) {
580 c.Check(h.Get("Access-Control-Allow-Methods"), Equals, "GET, HEAD, PUT, OPTIONS")
581 c.Check(h.Get("Access-Control-Allow-Origin"), Equals, "*")
582 c.Check(h.Get("Access-Control-Allow-Headers"), Equals, "Authorization, Content-Length, Content-Type, X-Keep-Desired-Replicas, X-Keep-Signature, X-Keep-Storage-Classes")
583 c.Check(h.Get("Access-Control-Expose-Headers"), Equals, "X-Keep-Locator, X-Keep-Replicas-Stored, X-Keep-Storage-Classes-Confirmed")