Merge branch '20619-arv-client-libfuse' closes #20619
[arvados.git] / services / keep-web / s3_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package keepweb
6
7 import (
8         "bytes"
9         "context"
10         "crypto/rand"
11         "crypto/sha256"
12         "fmt"
13         "io/ioutil"
14         "mime"
15         "net/http"
16         "net/http/httptest"
17         "net/url"
18         "os"
19         "os/exec"
20         "strings"
21         "sync"
22         "time"
23
24         "git.arvados.org/arvados.git/sdk/go/arvados"
25         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
26         "git.arvados.org/arvados.git/sdk/go/arvadostest"
27         "git.arvados.org/arvados.git/sdk/go/keepclient"
28         "github.com/AdRoll/goamz/aws"
29         "github.com/AdRoll/goamz/s3"
30         aws_aws "github.com/aws/aws-sdk-go/aws"
31         aws_credentials "github.com/aws/aws-sdk-go/aws/credentials"
32         aws_session "github.com/aws/aws-sdk-go/aws/session"
33         aws_s3 "github.com/aws/aws-sdk-go/service/s3"
34         check "gopkg.in/check.v1"
35 )
36
37 type s3stage struct {
38         arv        *arvados.Client
39         ac         *arvadosclient.ArvadosClient
40         kc         *keepclient.KeepClient
41         proj       arvados.Group
42         projbucket *s3.Bucket
43         subproj    arvados.Group
44         coll       arvados.Collection
45         collbucket *s3.Bucket
46 }
47
48 func (s *IntegrationSuite) s3setup(c *check.C) s3stage {
49         var proj, subproj arvados.Group
50         var coll arvados.Collection
51         arv := arvados.NewClientFromEnv()
52         arv.AuthToken = arvadostest.ActiveToken
53         err := arv.RequestAndDecode(&proj, "POST", "arvados/v1/groups", nil, map[string]interface{}{
54                 "group": map[string]interface{}{
55                         "group_class": "project",
56                         "name":        "keep-web s3 test",
57                         "properties": map[string]interface{}{
58                                 "project-properties-key": "project properties value",
59                         },
60                 },
61                 "ensure_unique_name": true,
62         })
63         c.Assert(err, check.IsNil)
64         err = arv.RequestAndDecode(&subproj, "POST", "arvados/v1/groups", nil, map[string]interface{}{
65                 "group": map[string]interface{}{
66                         "owner_uuid":  proj.UUID,
67                         "group_class": "project",
68                         "name":        "keep-web s3 test subproject",
69                         "properties": map[string]interface{}{
70                                 "subproject_properties_key": "subproject properties value",
71                                 "invalid header key":        "this value will not be returned because key contains spaces",
72                         },
73                 },
74         })
75         c.Assert(err, check.IsNil)
76         err = arv.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{
77                 "owner_uuid":    proj.UUID,
78                 "name":          "keep-web s3 test collection",
79                 "manifest_text": ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:emptyfile\n./emptydir d41d8cd98f00b204e9800998ecf8427e+0 0:0:.\n",
80                 "properties": map[string]interface{}{
81                         "string":   "string value",
82                         "array":    []string{"element1", "element2"},
83                         "object":   map[string]interface{}{"key": map[string]interface{}{"key2": "value⛵"}},
84                         "nonascii": "⛵",
85                         "newline":  "foo\r\nX-Bad: header",
86                         // This key cannot be expressed as a MIME
87                         // header key, so it will be silently skipped
88                         // (see "Inject" in PropertiesAsMetadata test)
89                         "a: a\r\nInject": "bogus",
90                 },
91         }})
92         c.Assert(err, check.IsNil)
93         ac, err := arvadosclient.New(arv)
94         c.Assert(err, check.IsNil)
95         kc, err := keepclient.MakeKeepClient(ac)
96         c.Assert(err, check.IsNil)
97         fs, err := coll.FileSystem(arv, kc)
98         c.Assert(err, check.IsNil)
99         f, err := fs.OpenFile("sailboat.txt", os.O_CREATE|os.O_WRONLY, 0644)
100         c.Assert(err, check.IsNil)
101         _, err = f.Write([]byte("⛵\n"))
102         c.Assert(err, check.IsNil)
103         err = f.Close()
104         c.Assert(err, check.IsNil)
105         err = fs.Sync()
106         c.Assert(err, check.IsNil)
107         err = arv.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+coll.UUID, nil, nil)
108         c.Assert(err, check.IsNil)
109
110         auth := aws.NewAuth(arvadostest.ActiveTokenUUID, arvadostest.ActiveToken, "", time.Now().Add(time.Hour))
111         region := aws.Region{
112                 Name:       "zzzzz",
113                 S3Endpoint: s.testServer.URL,
114         }
115         client := s3.New(*auth, region)
116         client.Signature = aws.V4Signature
117         return s3stage{
118                 arv:  arv,
119                 ac:   ac,
120                 kc:   kc,
121                 proj: proj,
122                 projbucket: &s3.Bucket{
123                         S3:   client,
124                         Name: proj.UUID,
125                 },
126                 subproj: subproj,
127                 coll:    coll,
128                 collbucket: &s3.Bucket{
129                         S3:   client,
130                         Name: coll.UUID,
131                 },
132         }
133 }
134
135 func (stage s3stage) teardown(c *check.C) {
136         if stage.coll.UUID != "" {
137                 err := stage.arv.RequestAndDecode(&stage.coll, "DELETE", "arvados/v1/collections/"+stage.coll.UUID, nil, nil)
138                 c.Check(err, check.IsNil)
139         }
140         if stage.proj.UUID != "" {
141                 err := stage.arv.RequestAndDecode(&stage.proj, "DELETE", "arvados/v1/groups/"+stage.proj.UUID, nil, nil)
142                 c.Check(err, check.IsNil)
143         }
144 }
145
146 func (s *IntegrationSuite) TestS3Signatures(c *check.C) {
147         stage := s.s3setup(c)
148         defer stage.teardown(c)
149
150         bucket := stage.collbucket
151         for _, trial := range []struct {
152                 success   bool
153                 signature int
154                 accesskey string
155                 secretkey string
156         }{
157                 {true, aws.V2Signature, arvadostest.ActiveToken, "none"},
158                 {true, aws.V2Signature, url.QueryEscape(arvadostest.ActiveTokenV2), "none"},
159                 {true, aws.V2Signature, strings.Replace(arvadostest.ActiveTokenV2, "/", "_", -1), "none"},
160                 {false, aws.V2Signature, "none", "none"},
161                 {false, aws.V2Signature, "none", arvadostest.ActiveToken},
162
163                 {true, aws.V4Signature, arvadostest.ActiveTokenUUID, arvadostest.ActiveToken},
164                 {true, aws.V4Signature, arvadostest.ActiveToken, arvadostest.ActiveToken},
165                 {true, aws.V4Signature, url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2)},
166                 {true, aws.V4Signature, strings.Replace(arvadostest.ActiveTokenV2, "/", "_", -1), strings.Replace(arvadostest.ActiveTokenV2, "/", "_", -1)},
167                 {false, aws.V4Signature, arvadostest.ActiveToken, ""},
168                 {false, aws.V4Signature, arvadostest.ActiveToken, "none"},
169                 {false, aws.V4Signature, "none", arvadostest.ActiveToken},
170                 {false, aws.V4Signature, "none", "none"},
171         } {
172                 c.Logf("%#v", trial)
173                 bucket.S3.Auth = *(aws.NewAuth(trial.accesskey, trial.secretkey, "", time.Now().Add(time.Hour)))
174                 bucket.S3.Signature = trial.signature
175                 _, err := bucket.GetReader("emptyfile")
176                 if trial.success {
177                         c.Check(err, check.IsNil)
178                 } else {
179                         c.Check(err, check.NotNil)
180                 }
181         }
182 }
183
184 func (s *IntegrationSuite) TestS3HeadBucket(c *check.C) {
185         stage := s.s3setup(c)
186         defer stage.teardown(c)
187
188         for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
189                 c.Logf("bucket %s", bucket.Name)
190                 exists, err := bucket.Exists("")
191                 c.Check(err, check.IsNil)
192                 c.Check(exists, check.Equals, true)
193         }
194 }
195
196 func (s *IntegrationSuite) TestS3CollectionGetObject(c *check.C) {
197         stage := s.s3setup(c)
198         defer stage.teardown(c)
199         s.testS3GetObject(c, stage.collbucket, "")
200 }
201 func (s *IntegrationSuite) TestS3ProjectGetObject(c *check.C) {
202         stage := s.s3setup(c)
203         defer stage.teardown(c)
204         s.testS3GetObject(c, stage.projbucket, stage.coll.Name+"/")
205 }
206 func (s *IntegrationSuite) testS3GetObject(c *check.C, bucket *s3.Bucket, prefix string) {
207         rdr, err := bucket.GetReader(prefix + "emptyfile")
208         c.Assert(err, check.IsNil)
209         buf, err := ioutil.ReadAll(rdr)
210         c.Check(err, check.IsNil)
211         c.Check(len(buf), check.Equals, 0)
212         err = rdr.Close()
213         c.Check(err, check.IsNil)
214
215         // GetObject
216         rdr, err = bucket.GetReader(prefix + "missingfile")
217         c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
218         c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
219         c.Check(err, check.ErrorMatches, `The specified key does not exist.`)
220
221         // HeadObject
222         exists, err := bucket.Exists(prefix + "missingfile")
223         c.Check(err, check.IsNil)
224         c.Check(exists, check.Equals, false)
225
226         // GetObject
227         rdr, err = bucket.GetReader(prefix + "sailboat.txt")
228         c.Assert(err, check.IsNil)
229         buf, err = ioutil.ReadAll(rdr)
230         c.Check(err, check.IsNil)
231         c.Check(buf, check.DeepEquals, []byte("⛵\n"))
232         err = rdr.Close()
233         c.Check(err, check.IsNil)
234
235         // HeadObject
236         resp, err := bucket.Head(prefix+"sailboat.txt", nil)
237         c.Check(err, check.IsNil)
238         c.Check(resp.StatusCode, check.Equals, http.StatusOK)
239         c.Check(resp.ContentLength, check.Equals, int64(4))
240
241         // HeadObject with superfluous leading slashes
242         exists, err = bucket.Exists(prefix + "//sailboat.txt")
243         c.Check(err, check.IsNil)
244         c.Check(exists, check.Equals, true)
245 }
246
247 func (s *IntegrationSuite) checkMetaEquals(c *check.C, hdr http.Header, expect map[string]string) {
248         got := map[string]string{}
249         for hk, hv := range hdr {
250                 if k := strings.TrimPrefix(hk, "X-Amz-Meta-"); k != hk && len(hv) == 1 {
251                         got[k] = hv[0]
252                 }
253         }
254         c.Check(got, check.DeepEquals, expect)
255 }
256
257 func (s *IntegrationSuite) TestS3PropertiesAsMetadata(c *check.C) {
258         stage := s.s3setup(c)
259         defer stage.teardown(c)
260
261         expectCollectionTags := map[string]string{
262                 "String":   "string value",
263                 "Array":    `["element1","element2"]`,
264                 "Object":   mime.BEncoding.Encode("UTF-8", `{"key":{"key2":"value⛵"}}`),
265                 "Nonascii": "=?UTF-8?b?4pu1?=",
266                 "Newline":  mime.BEncoding.Encode("UTF-8", "foo\r\nX-Bad: header"),
267         }
268         expectSubprojectTags := map[string]string{
269                 "Subproject_properties_key": "subproject properties value",
270         }
271         expectProjectTags := map[string]string{
272                 "Project-Properties-Key": "project properties value",
273         }
274
275         c.Log("HEAD object with metadata from collection")
276         resp, err := stage.collbucket.Head("sailboat.txt", nil)
277         c.Assert(err, check.IsNil)
278         s.checkMetaEquals(c, resp.Header, expectCollectionTags)
279
280         c.Log("GET object with metadata from collection")
281         rdr, hdr, err := stage.collbucket.GetReaderWithHeaders("sailboat.txt")
282         c.Assert(err, check.IsNil)
283         content, err := ioutil.ReadAll(rdr)
284         c.Check(err, check.IsNil)
285         rdr.Close()
286         c.Check(content, check.HasLen, 4)
287         s.checkMetaEquals(c, hdr, expectCollectionTags)
288         c.Check(hdr["Inject"], check.IsNil)
289
290         c.Log("HEAD bucket with metadata from collection")
291         resp, err = stage.collbucket.Head("/", nil)
292         c.Assert(err, check.IsNil)
293         s.checkMetaEquals(c, resp.Header, expectCollectionTags)
294
295         c.Log("HEAD directory placeholder with metadata from collection")
296         resp, err = stage.projbucket.Head("keep-web s3 test collection/", nil)
297         c.Assert(err, check.IsNil)
298         s.checkMetaEquals(c, resp.Header, expectCollectionTags)
299
300         c.Log("HEAD file with metadata from collection")
301         resp, err = stage.projbucket.Head("keep-web s3 test collection/sailboat.txt", nil)
302         c.Assert(err, check.IsNil)
303         s.checkMetaEquals(c, resp.Header, expectCollectionTags)
304
305         c.Log("HEAD directory placeholder with metadata from subproject")
306         resp, err = stage.projbucket.Head("keep-web s3 test subproject/", nil)
307         c.Assert(err, check.IsNil)
308         s.checkMetaEquals(c, resp.Header, expectSubprojectTags)
309
310         c.Log("HEAD bucket with metadata from project")
311         resp, err = stage.projbucket.Head("/", nil)
312         c.Assert(err, check.IsNil)
313         s.checkMetaEquals(c, resp.Header, expectProjectTags)
314 }
315
316 func (s *IntegrationSuite) TestS3CollectionPutObjectSuccess(c *check.C) {
317         stage := s.s3setup(c)
318         defer stage.teardown(c)
319         s.testS3PutObjectSuccess(c, stage.collbucket, "", stage.coll.UUID)
320 }
321 func (s *IntegrationSuite) TestS3ProjectPutObjectSuccess(c *check.C) {
322         stage := s.s3setup(c)
323         defer stage.teardown(c)
324         s.testS3PutObjectSuccess(c, stage.projbucket, stage.coll.Name+"/", stage.coll.UUID)
325 }
326 func (s *IntegrationSuite) testS3PutObjectSuccess(c *check.C, bucket *s3.Bucket, prefix string, collUUID string) {
327         for _, trial := range []struct {
328                 path        string
329                 size        int
330                 contentType string
331         }{
332                 {
333                         path:        "newfile",
334                         size:        128000000,
335                         contentType: "application/octet-stream",
336                 }, {
337                         path:        "newdir/newfile",
338                         size:        1 << 26,
339                         contentType: "application/octet-stream",
340                 }, {
341                         path:        "/aaa",
342                         size:        2,
343                         contentType: "application/octet-stream",
344                 }, {
345                         path:        "//bbb",
346                         size:        2,
347                         contentType: "application/octet-stream",
348                 }, {
349                         path:        "ccc//",
350                         size:        0,
351                         contentType: "application/x-directory",
352                 }, {
353                         path:        "newdir1/newdir2/newfile",
354                         size:        0,
355                         contentType: "application/octet-stream",
356                 }, {
357                         path:        "newdir1/newdir2/newdir3/",
358                         size:        0,
359                         contentType: "application/x-directory",
360                 },
361         } {
362                 c.Logf("=== %v", trial)
363
364                 objname := prefix + trial.path
365
366                 _, err := bucket.GetReader(objname)
367                 if !c.Check(err, check.NotNil) {
368                         continue
369                 }
370                 c.Check(err.(*s3.Error).StatusCode, check.Equals, http.StatusNotFound)
371                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
372                 if !c.Check(err, check.ErrorMatches, `The specified key does not exist.`) {
373                         continue
374                 }
375
376                 buf := make([]byte, trial.size)
377                 rand.Read(buf)
378
379                 err = bucket.PutReader(objname, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
380                 c.Check(err, check.IsNil)
381
382                 rdr, err := bucket.GetReader(objname)
383                 if strings.HasSuffix(trial.path, "/") && !s.handler.Cluster.Collections.S3FolderObjects {
384                         c.Check(err, check.NotNil)
385                         continue
386                 } else if !c.Check(err, check.IsNil) {
387                         continue
388                 }
389                 buf2, err := ioutil.ReadAll(rdr)
390                 c.Check(err, check.IsNil)
391                 c.Check(buf2, check.HasLen, len(buf))
392                 c.Check(bytes.Equal(buf, buf2), check.Equals, true)
393
394                 // Check that the change is immediately visible via
395                 // (non-S3) webdav request.
396                 _, resp := s.do("GET", "http://"+collUUID+".keep-web.example/"+trial.path, arvadostest.ActiveTokenV2, nil)
397                 c.Check(resp.Code, check.Equals, http.StatusOK)
398                 if !strings.HasSuffix(trial.path, "/") {
399                         c.Check(resp.Body.Len(), check.Equals, trial.size)
400                 }
401         }
402 }
403
404 func (s *IntegrationSuite) TestS3ProjectPutObjectNotSupported(c *check.C) {
405         stage := s.s3setup(c)
406         defer stage.teardown(c)
407         bucket := stage.projbucket
408
409         for _, trial := range []struct {
410                 path        string
411                 size        int
412                 contentType string
413         }{
414                 {
415                         path:        "newfile",
416                         size:        1234,
417                         contentType: "application/octet-stream",
418                 }, {
419                         path:        "newdir/newfile",
420                         size:        1234,
421                         contentType: "application/octet-stream",
422                 }, {
423                         path:        "newdir2/",
424                         size:        0,
425                         contentType: "application/x-directory",
426                 },
427         } {
428                 c.Logf("=== %v", trial)
429
430                 _, err := bucket.GetReader(trial.path)
431                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
432                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
433                 c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
434
435                 buf := make([]byte, trial.size)
436                 rand.Read(buf)
437
438                 err = bucket.PutReader(trial.path, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
439                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 400)
440                 c.Check(err.(*s3.Error).Code, check.Equals, `InvalidArgument`)
441                 c.Check(err, check.ErrorMatches, `(mkdir "/by_id/zzzzz-j7d0g-[a-z0-9]{15}/newdir2?"|open "/zzzzz-j7d0g-[a-z0-9]{15}/newfile") failed: invalid (argument|operation)`)
442
443                 _, err = bucket.GetReader(trial.path)
444                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
445                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
446                 c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
447         }
448 }
449
450 func (s *IntegrationSuite) TestS3CollectionDeleteObject(c *check.C) {
451         stage := s.s3setup(c)
452         defer stage.teardown(c)
453         s.testS3DeleteObject(c, stage.collbucket, "")
454 }
455 func (s *IntegrationSuite) TestS3ProjectDeleteObject(c *check.C) {
456         stage := s.s3setup(c)
457         defer stage.teardown(c)
458         s.testS3DeleteObject(c, stage.projbucket, stage.coll.Name+"/")
459 }
460 func (s *IntegrationSuite) testS3DeleteObject(c *check.C, bucket *s3.Bucket, prefix string) {
461         s.handler.Cluster.Collections.S3FolderObjects = true
462         for _, trial := range []struct {
463                 path string
464         }{
465                 {"/"},
466                 {"nonexistentfile"},
467                 {"emptyfile"},
468                 {"sailboat.txt"},
469                 {"sailboat.txt/"},
470                 {"emptydir"},
471                 {"emptydir/"},
472         } {
473                 objname := prefix + trial.path
474                 comment := check.Commentf("objname %q", objname)
475
476                 err := bucket.Del(objname)
477                 if trial.path == "/" {
478                         c.Check(err, check.NotNil)
479                         continue
480                 }
481                 c.Check(err, check.IsNil, comment)
482                 _, err = bucket.GetReader(objname)
483                 c.Check(err, check.NotNil, comment)
484         }
485 }
486
487 func (s *IntegrationSuite) TestS3CollectionPutObjectFailure(c *check.C) {
488         stage := s.s3setup(c)
489         defer stage.teardown(c)
490         s.testS3PutObjectFailure(c, stage.collbucket, "")
491 }
492 func (s *IntegrationSuite) TestS3ProjectPutObjectFailure(c *check.C) {
493         stage := s.s3setup(c)
494         defer stage.teardown(c)
495         s.testS3PutObjectFailure(c, stage.projbucket, stage.coll.Name+"/")
496 }
497 func (s *IntegrationSuite) testS3PutObjectFailure(c *check.C, bucket *s3.Bucket, prefix string) {
498         s.handler.Cluster.Collections.S3FolderObjects = false
499
500         var wg sync.WaitGroup
501         for _, trial := range []struct {
502                 path string
503         }{
504                 {
505                         path: "emptyfile/newname", // emptyfile exists, see s3setup()
506                 }, {
507                         path: "emptyfile/", // emptyfile exists, see s3setup()
508                 }, {
509                         path: "emptydir", // dir already exists, see s3setup()
510                 }, {
511                         path: "emptydir/",
512                 }, {
513                         path: "emptydir//",
514                 }, {
515                         path: "newdir/",
516                 }, {
517                         path: "newdir//",
518                 }, {
519                         path: "/",
520                 }, {
521                         path: "//",
522                 }, {
523                         path: "",
524                 },
525         } {
526                 trial := trial
527                 wg.Add(1)
528                 go func() {
529                         defer wg.Done()
530                         c.Logf("=== %v", trial)
531
532                         objname := prefix + trial.path
533
534                         buf := make([]byte, 1234)
535                         rand.Read(buf)
536
537                         err := bucket.PutReader(objname, bytes.NewReader(buf), int64(len(buf)), "application/octet-stream", s3.Private, s3.Options{})
538                         if !c.Check(err, check.ErrorMatches, `(invalid object name.*|open ".*" failed.*|object name conflicts with existing object|Missing object name in PUT request.)`, check.Commentf("PUT %q should fail", objname)) {
539                                 return
540                         }
541
542                         if objname != "" && objname != "/" {
543                                 _, err = bucket.GetReader(objname)
544                                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
545                                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
546                                 c.Check(err, check.ErrorMatches, `The specified key does not exist.`, check.Commentf("GET %q should return 404", objname))
547                         }
548                 }()
549         }
550         wg.Wait()
551 }
552
553 func (stage *s3stage) writeBigDirs(c *check.C, dirs int, filesPerDir int) {
554         fs, err := stage.coll.FileSystem(stage.arv, stage.kc)
555         c.Assert(err, check.IsNil)
556         for d := 0; d < dirs; d++ {
557                 dir := fmt.Sprintf("dir%d", d)
558                 c.Assert(fs.Mkdir(dir, 0755), check.IsNil)
559                 for i := 0; i < filesPerDir; i++ {
560                         f, err := fs.OpenFile(fmt.Sprintf("%s/file%d.txt", dir, i), os.O_CREATE|os.O_WRONLY, 0644)
561                         c.Assert(err, check.IsNil)
562                         c.Assert(f.Close(), check.IsNil)
563                 }
564         }
565         c.Assert(fs.Sync(), check.IsNil)
566 }
567
568 func (s *IntegrationSuite) sign(c *check.C, req *http.Request, key, secret string) {
569         scope := "20200202/zzzzz/service/aws4_request"
570         signedHeaders := "date"
571         req.Header.Set("Date", time.Now().UTC().Format(time.RFC1123))
572         stringToSign, err := s3stringToSign(s3SignAlgorithm, scope, signedHeaders, req)
573         c.Assert(err, check.IsNil)
574         sig, err := s3signature(secret, scope, signedHeaders, stringToSign)
575         c.Assert(err, check.IsNil)
576         req.Header.Set("Authorization", s3SignAlgorithm+" Credential="+key+"/"+scope+", SignedHeaders="+signedHeaders+", Signature="+sig)
577 }
578
579 func (s *IntegrationSuite) TestS3VirtualHostStyleRequests(c *check.C) {
580         stage := s.s3setup(c)
581         defer stage.teardown(c)
582         for _, trial := range []struct {
583                 url            string
584                 method         string
585                 body           string
586                 responseCode   int
587                 responseRegexp []string
588         }{
589                 {
590                         url:            "https://" + stage.collbucket.Name + ".example.com/",
591                         method:         "GET",
592                         responseCode:   http.StatusOK,
593                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
594                 },
595                 {
596                         url:            "https://" + strings.Replace(stage.coll.PortableDataHash, "+", "-", -1) + ".example.com/",
597                         method:         "GET",
598                         responseCode:   http.StatusOK,
599                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
600                 },
601                 {
602                         url:            "https://" + stage.projbucket.Name + ".example.com/?prefix=" + stage.coll.Name + "/&delimiter=/",
603                         method:         "GET",
604                         responseCode:   http.StatusOK,
605                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
606                 },
607                 {
608                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/sailboat.txt",
609                         method:         "GET",
610                         responseCode:   http.StatusOK,
611                         responseRegexp: []string{`⛵\n`},
612                 },
613                 {
614                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
615                         method:       "PUT",
616                         body:         "boop",
617                         responseCode: http.StatusOK,
618                 },
619                 {
620                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
621                         method:         "GET",
622                         responseCode:   http.StatusOK,
623                         responseRegexp: []string{`boop`},
624                 },
625                 {
626                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
627                         method:       "GET",
628                         responseCode: http.StatusNotFound,
629                 },
630                 {
631                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
632                         method:       "PUT",
633                         body:         "boop",
634                         responseCode: http.StatusOK,
635                 },
636                 {
637                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
638                         method:         "GET",
639                         responseCode:   http.StatusOK,
640                         responseRegexp: []string{`boop`},
641                 },
642         } {
643                 url, err := url.Parse(trial.url)
644                 c.Assert(err, check.IsNil)
645                 req, err := http.NewRequest(trial.method, url.String(), bytes.NewReader([]byte(trial.body)))
646                 c.Assert(err, check.IsNil)
647                 s.sign(c, req, arvadostest.ActiveTokenUUID, arvadostest.ActiveToken)
648                 rr := httptest.NewRecorder()
649                 s.handler.ServeHTTP(rr, req)
650                 resp := rr.Result()
651                 c.Check(resp.StatusCode, check.Equals, trial.responseCode)
652                 body, err := ioutil.ReadAll(resp.Body)
653                 c.Assert(err, check.IsNil)
654                 for _, re := range trial.responseRegexp {
655                         c.Check(string(body), check.Matches, re)
656                 }
657         }
658 }
659
660 func (s *IntegrationSuite) TestS3NormalizeURIForSignature(c *check.C) {
661         stage := s.s3setup(c)
662         defer stage.teardown(c)
663         for _, trial := range []struct {
664                 rawPath        string
665                 normalizedPath string
666         }{
667                 {"/foo", "/foo"},                           // boring case
668                 {"/foo%5fbar", "/foo_bar"},                 // _ must not be escaped
669                 {"/foo%2fbar", "/foo/bar"},                 // / must not be escaped
670                 {"/(foo)/[];,", "/%28foo%29/%5B%5D%3B%2C"}, // ()[];, must be escaped
671                 {"/foo%5bbar", "/foo%5Bbar"},               // %XX must be uppercase
672                 {"//foo///.bar", "/foo/.bar"},              // "//" and "///" must be squashed to "/"
673         } {
674                 c.Logf("trial %q", trial)
675
676                 date := time.Now().UTC().Format("20060102T150405Z")
677                 scope := "20200202/zzzzz/S3/aws4_request"
678                 canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", "GET", trial.normalizedPath, "", "host:host.example.com\n", "host", "")
679                 c.Logf("canonicalRequest %q", canonicalRequest)
680                 expect := fmt.Sprintf("%s\n%s\n%s\n%s", s3SignAlgorithm, date, scope, hashdigest(sha256.New(), canonicalRequest))
681                 c.Logf("expected stringToSign %q", expect)
682
683                 req, err := http.NewRequest("GET", "https://host.example.com"+trial.rawPath, nil)
684                 req.Header.Set("X-Amz-Date", date)
685                 req.Host = "host.example.com"
686                 c.Assert(err, check.IsNil)
687
688                 obtained, err := s3stringToSign(s3SignAlgorithm, scope, "host", req)
689                 if !c.Check(err, check.IsNil) {
690                         continue
691                 }
692                 c.Check(obtained, check.Equals, expect)
693         }
694 }
695
696 func (s *IntegrationSuite) TestS3GetBucketLocation(c *check.C) {
697         stage := s.s3setup(c)
698         defer stage.teardown(c)
699         for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
700                 req, err := http.NewRequest("GET", bucket.URL("/"), nil)
701                 c.Check(err, check.IsNil)
702                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
703                 req.URL.RawQuery = "location"
704                 resp, err := http.DefaultClient.Do(req)
705                 c.Assert(err, check.IsNil)
706                 c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
707                 buf, err := ioutil.ReadAll(resp.Body)
708                 c.Assert(err, check.IsNil)
709                 c.Check(string(buf), check.Equals, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<LocationConstraint><LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">zzzzz</LocationConstraint></LocationConstraint>\n")
710         }
711 }
712
713 func (s *IntegrationSuite) TestS3GetBucketVersioning(c *check.C) {
714         stage := s.s3setup(c)
715         defer stage.teardown(c)
716         for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
717                 req, err := http.NewRequest("GET", bucket.URL("/"), nil)
718                 c.Check(err, check.IsNil)
719                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
720                 req.URL.RawQuery = "versioning"
721                 resp, err := http.DefaultClient.Do(req)
722                 c.Assert(err, check.IsNil)
723                 c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
724                 buf, err := ioutil.ReadAll(resp.Body)
725                 c.Assert(err, check.IsNil)
726                 c.Check(string(buf), check.Equals, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>\n")
727         }
728 }
729
730 func (s *IntegrationSuite) TestS3UnsupportedAPIs(c *check.C) {
731         stage := s.s3setup(c)
732         defer stage.teardown(c)
733         for _, trial := range []struct {
734                 method   string
735                 path     string
736                 rawquery string
737         }{
738                 {"GET", "/", "acl&versionId=1234"},    // GetBucketAcl
739                 {"GET", "/foo", "acl&versionId=1234"}, // GetObjectAcl
740                 {"PUT", "/", "acl"},                   // PutBucketAcl
741                 {"PUT", "/foo", "acl"},                // PutObjectAcl
742                 {"DELETE", "/", "tagging"},            // DeleteBucketTagging
743                 {"DELETE", "/foo", "tagging"},         // DeleteObjectTagging
744         } {
745                 for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
746                         c.Logf("trial %v bucket %v", trial, bucket)
747                         req, err := http.NewRequest(trial.method, bucket.URL(trial.path), nil)
748                         c.Check(err, check.IsNil)
749                         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
750                         req.URL.RawQuery = trial.rawquery
751                         resp, err := http.DefaultClient.Do(req)
752                         c.Assert(err, check.IsNil)
753                         c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
754                         buf, err := ioutil.ReadAll(resp.Body)
755                         c.Assert(err, check.IsNil)
756                         c.Check(string(buf), check.Matches, "(?ms).*InvalidRequest.*API not supported.*")
757                 }
758         }
759 }
760
761 // If there are no CommonPrefixes entries, the CommonPrefixes XML tag
762 // should not appear at all.
763 func (s *IntegrationSuite) TestS3ListNoCommonPrefixes(c *check.C) {
764         stage := s.s3setup(c)
765         defer stage.teardown(c)
766
767         req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
768         c.Assert(err, check.IsNil)
769         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
770         req.URL.RawQuery = "prefix=asdfasdfasdf&delimiter=/"
771         resp, err := http.DefaultClient.Do(req)
772         c.Assert(err, check.IsNil)
773         buf, err := ioutil.ReadAll(resp.Body)
774         c.Assert(err, check.IsNil)
775         c.Check(string(buf), check.Not(check.Matches), `(?ms).*CommonPrefixes.*`)
776 }
777
778 // If there is no delimiter in the request, or the results are not
779 // truncated, the NextMarker XML tag should not appear in the response
780 // body.
781 func (s *IntegrationSuite) TestS3ListNoNextMarker(c *check.C) {
782         stage := s.s3setup(c)
783         defer stage.teardown(c)
784
785         for _, query := range []string{"prefix=e&delimiter=/", ""} {
786                 req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
787                 c.Assert(err, check.IsNil)
788                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
789                 req.URL.RawQuery = query
790                 resp, err := http.DefaultClient.Do(req)
791                 c.Assert(err, check.IsNil)
792                 buf, err := ioutil.ReadAll(resp.Body)
793                 c.Assert(err, check.IsNil)
794                 c.Check(string(buf), check.Not(check.Matches), `(?ms).*NextMarker.*`)
795         }
796 }
797
798 // List response should include KeyCount field.
799 func (s *IntegrationSuite) TestS3ListKeyCount(c *check.C) {
800         stage := s.s3setup(c)
801         defer stage.teardown(c)
802
803         req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
804         c.Assert(err, check.IsNil)
805         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
806         req.URL.RawQuery = "prefix=&delimiter=/"
807         resp, err := http.DefaultClient.Do(req)
808         c.Assert(err, check.IsNil)
809         buf, err := ioutil.ReadAll(resp.Body)
810         c.Assert(err, check.IsNil)
811         c.Check(string(buf), check.Matches, `(?ms).*<KeyCount>2</KeyCount>.*`)
812 }
813
814 func (s *IntegrationSuite) TestS3CollectionList(c *check.C) {
815         stage := s.s3setup(c)
816         defer stage.teardown(c)
817
818         var markers int
819         for markers, s.handler.Cluster.Collections.S3FolderObjects = range []bool{false, true} {
820                 dirs := 2
821                 filesPerDir := 1001
822                 stage.writeBigDirs(c, dirs, filesPerDir)
823                 // Total # objects is:
824                 //                 2 file entries from s3setup (emptyfile and sailboat.txt)
825                 //                +1 fake "directory" marker from s3setup (emptydir) (if enabled)
826                 //             +dirs fake "directory" marker from writeBigDirs (dir0/, dir1/) (if enabled)
827                 // +filesPerDir*dirs file entries from writeBigDirs (dir0/file0.txt, etc.)
828                 s.testS3List(c, stage.collbucket, "", 4000, markers+2+(filesPerDir+markers)*dirs)
829                 s.testS3List(c, stage.collbucket, "", 131, markers+2+(filesPerDir+markers)*dirs)
830                 s.testS3List(c, stage.collbucket, "dir0/", 71, filesPerDir+markers)
831         }
832 }
833 func (s *IntegrationSuite) testS3List(c *check.C, bucket *s3.Bucket, prefix string, pageSize, expectFiles int) {
834         c.Logf("testS3List: prefix=%q pageSize=%d S3FolderObjects=%v", prefix, pageSize, s.handler.Cluster.Collections.S3FolderObjects)
835         expectPageSize := pageSize
836         if expectPageSize > 1000 {
837                 expectPageSize = 1000
838         }
839         gotKeys := map[string]s3.Key{}
840         nextMarker := ""
841         pages := 0
842         for {
843                 resp, err := bucket.List(prefix, "", nextMarker, pageSize)
844                 if !c.Check(err, check.IsNil) {
845                         break
846                 }
847                 c.Check(len(resp.Contents) <= expectPageSize, check.Equals, true)
848                 if pages++; !c.Check(pages <= (expectFiles/expectPageSize)+1, check.Equals, true) {
849                         break
850                 }
851                 for _, key := range resp.Contents {
852                         gotKeys[key.Key] = key
853                         if strings.Contains(key.Key, "sailboat.txt") {
854                                 c.Check(key.Size, check.Equals, int64(4))
855                         }
856                 }
857                 if !resp.IsTruncated {
858                         c.Check(resp.NextMarker, check.Equals, "")
859                         break
860                 }
861                 if !c.Check(resp.NextMarker, check.Not(check.Equals), "") {
862                         break
863                 }
864                 nextMarker = resp.NextMarker
865         }
866         c.Check(len(gotKeys), check.Equals, expectFiles)
867 }
868
869 func (s *IntegrationSuite) TestS3CollectionListRollup(c *check.C) {
870         for _, s.handler.Cluster.Collections.S3FolderObjects = range []bool{false, true} {
871                 s.testS3CollectionListRollup(c)
872         }
873 }
874
875 func (s *IntegrationSuite) testS3CollectionListRollup(c *check.C) {
876         stage := s.s3setup(c)
877         defer stage.teardown(c)
878
879         dirs := 2
880         filesPerDir := 500
881         stage.writeBigDirs(c, dirs, filesPerDir)
882         err := stage.collbucket.PutReader("dingbats", &bytes.Buffer{}, 0, "application/octet-stream", s3.Private, s3.Options{})
883         c.Assert(err, check.IsNil)
884         var allfiles []string
885         for marker := ""; ; {
886                 resp, err := stage.collbucket.List("", "", marker, 20000)
887                 c.Check(err, check.IsNil)
888                 for _, key := range resp.Contents {
889                         if len(allfiles) == 0 || allfiles[len(allfiles)-1] != key.Key {
890                                 allfiles = append(allfiles, key.Key)
891                         }
892                 }
893                 marker = resp.NextMarker
894                 if marker == "" {
895                         break
896                 }
897         }
898         markers := 0
899         if s.handler.Cluster.Collections.S3FolderObjects {
900                 markers = 1
901         }
902         c.Check(allfiles, check.HasLen, dirs*(filesPerDir+markers)+3+markers)
903
904         gotDirMarker := map[string]bool{}
905         for _, name := range allfiles {
906                 isDirMarker := strings.HasSuffix(name, "/")
907                 if markers == 0 {
908                         c.Check(isDirMarker, check.Equals, false, check.Commentf("name %q", name))
909                 } else if isDirMarker {
910                         gotDirMarker[name] = true
911                 } else if i := strings.LastIndex(name, "/"); i >= 0 {
912                         c.Check(gotDirMarker[name[:i+1]], check.Equals, true, check.Commentf("name %q", name))
913                         gotDirMarker[name[:i+1]] = true // skip redundant complaints about this dir marker
914                 }
915         }
916
917         for _, trial := range []struct {
918                 prefix    string
919                 delimiter string
920                 marker    string
921         }{
922                 {"", "", ""},
923                 {"di", "/", ""},
924                 {"di", "r", ""},
925                 {"di", "n", ""},
926                 {"dir0", "/", ""},
927                 {"dir0/", "/", ""},
928                 {"dir0/f", "/", ""},
929                 {"dir0", "", ""},
930                 {"dir0/", "", ""},
931                 {"dir0/f", "", ""},
932                 {"dir0", "/", "dir0/file14.txt"},       // no commonprefixes
933                 {"", "", "dir0/file14.txt"},            // middle page, skip walking dir1
934                 {"", "", "dir1/file14.txt"},            // middle page, skip walking dir0
935                 {"", "", "dir1/file498.txt"},           // last page of results
936                 {"dir1/file", "", "dir1/file498.txt"},  // last page of results, with prefix
937                 {"dir1/file", "/", "dir1/file498.txt"}, // last page of results, with prefix + delimiter
938                 {"dir1", "Z", "dir1/file498.txt"},      // delimiter "Z" never appears
939                 {"dir2", "/", ""},                      // prefix "dir2" does not exist
940                 {"", "/", ""},
941         } {
942                 c.Logf("\n\n=== trial %+v markers=%d", trial, markers)
943
944                 maxKeys := 20
945                 resp, err := stage.collbucket.List(trial.prefix, trial.delimiter, trial.marker, maxKeys)
946                 c.Check(err, check.IsNil)
947                 if resp.IsTruncated && trial.delimiter == "" {
948                         // goamz List method fills in the missing
949                         // NextMarker field if resp.IsTruncated, so
950                         // now we can't really tell whether it was
951                         // sent by the server or by goamz. In cases
952                         // where it should be empty but isn't, assume
953                         // it's goamz's fault.
954                         resp.NextMarker = ""
955                 }
956
957                 var expectKeys []string
958                 var expectPrefixes []string
959                 var expectNextMarker string
960                 var expectTruncated bool
961                 for _, key := range allfiles {
962                         full := len(expectKeys)+len(expectPrefixes) >= maxKeys
963                         if !strings.HasPrefix(key, trial.prefix) || key < trial.marker {
964                                 continue
965                         } else if idx := strings.Index(key[len(trial.prefix):], trial.delimiter); trial.delimiter != "" && idx >= 0 {
966                                 prefix := key[:len(trial.prefix)+idx+1]
967                                 if len(expectPrefixes) > 0 && expectPrefixes[len(expectPrefixes)-1] == prefix {
968                                         // same prefix as previous key
969                                 } else if full {
970                                         expectNextMarker = key
971                                         expectTruncated = true
972                                 } else {
973                                         expectPrefixes = append(expectPrefixes, prefix)
974                                 }
975                         } else if full {
976                                 if trial.delimiter != "" {
977                                         expectNextMarker = key
978                                 }
979                                 expectTruncated = true
980                                 break
981                         } else {
982                                 expectKeys = append(expectKeys, key)
983                         }
984                 }
985
986                 var gotKeys []string
987                 for _, key := range resp.Contents {
988                         gotKeys = append(gotKeys, key.Key)
989                 }
990                 var gotPrefixes []string
991                 for _, prefix := range resp.CommonPrefixes {
992                         gotPrefixes = append(gotPrefixes, prefix)
993                 }
994                 commentf := check.Commentf("trial %+v markers=%d", trial, markers)
995                 c.Check(gotKeys, check.DeepEquals, expectKeys, commentf)
996                 c.Check(gotPrefixes, check.DeepEquals, expectPrefixes, commentf)
997                 c.Check(resp.NextMarker, check.Equals, expectNextMarker, commentf)
998                 c.Check(resp.IsTruncated, check.Equals, expectTruncated, commentf)
999                 c.Logf("=== trial %+v keys %q prefixes %q nextMarker %q", trial, gotKeys, gotPrefixes, resp.NextMarker)
1000         }
1001 }
1002
1003 func (s *IntegrationSuite) TestS3ListObjectsV2(c *check.C) {
1004         stage := s.s3setup(c)
1005         defer stage.teardown(c)
1006         dirs := 2
1007         filesPerDir := 40
1008         stage.writeBigDirs(c, dirs, filesPerDir)
1009
1010         sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
1011                 Region:           aws_aws.String("auto"),
1012                 Endpoint:         aws_aws.String(s.testServer.URL),
1013                 Credentials:      aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
1014                 S3ForcePathStyle: aws_aws.Bool(true),
1015         }))
1016
1017         stringOrNil := func(s string) *string {
1018                 if s == "" {
1019                         return nil
1020                 } else {
1021                         return &s
1022                 }
1023         }
1024
1025         client := aws_s3.New(sess)
1026         ctx := context.Background()
1027
1028         for _, trial := range []struct {
1029                 prefix               string
1030                 delimiter            string
1031                 startAfter           string
1032                 maxKeys              int
1033                 expectKeys           int
1034                 expectCommonPrefixes map[string]bool
1035         }{
1036                 {
1037                         // Expect {filesPerDir plus the dir itself}
1038                         // for each dir, plus emptydir, emptyfile, and
1039                         // sailboat.txt.
1040                         expectKeys: (filesPerDir+1)*dirs + 3,
1041                 },
1042                 {
1043                         maxKeys:    15,
1044                         expectKeys: (filesPerDir+1)*dirs + 3,
1045                 },
1046                 {
1047                         startAfter: "dir0/z",
1048                         maxKeys:    15,
1049                         // Expect {filesPerDir plus the dir itself}
1050                         // for each dir except dir0, plus emptydir,
1051                         // emptyfile, and sailboat.txt.
1052                         expectKeys: (filesPerDir+1)*(dirs-1) + 3,
1053                 },
1054                 {
1055                         maxKeys:              1,
1056                         delimiter:            "/",
1057                         expectKeys:           2, // emptyfile, sailboat.txt
1058                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
1059                 },
1060                 {
1061                         startAfter:           "dir0/z",
1062                         maxKeys:              15,
1063                         delimiter:            "/",
1064                         expectKeys:           2, // emptyfile, sailboat.txt
1065                         expectCommonPrefixes: map[string]bool{"dir1/": true, "emptydir/": true},
1066                 },
1067                 {
1068                         startAfter:           "dir0/file10.txt",
1069                         maxKeys:              15,
1070                         delimiter:            "/",
1071                         expectKeys:           2,
1072                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
1073                 },
1074                 {
1075                         startAfter:           "dir0/file10.txt",
1076                         maxKeys:              15,
1077                         prefix:               "d",
1078                         delimiter:            "/",
1079                         expectKeys:           0,
1080                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true},
1081                 },
1082         } {
1083                 c.Logf("[trial %+v]", trial)
1084                 params := aws_s3.ListObjectsV2Input{
1085                         Bucket:     aws_aws.String(stage.collbucket.Name),
1086                         Prefix:     stringOrNil(trial.prefix),
1087                         Delimiter:  stringOrNil(trial.delimiter),
1088                         StartAfter: stringOrNil(trial.startAfter),
1089                         MaxKeys:    aws_aws.Int64(int64(trial.maxKeys)),
1090                 }
1091                 keySeen := map[string]bool{}
1092                 prefixSeen := map[string]bool{}
1093                 for {
1094                         result, err := client.ListObjectsV2WithContext(ctx, &params)
1095                         if !c.Check(err, check.IsNil) {
1096                                 break
1097                         }
1098                         c.Check(result.Name, check.DeepEquals, aws_aws.String(stage.collbucket.Name))
1099                         c.Check(result.Prefix, check.DeepEquals, aws_aws.String(trial.prefix))
1100                         c.Check(result.Delimiter, check.DeepEquals, aws_aws.String(trial.delimiter))
1101                         // The following two fields are expected to be
1102                         // nil (i.e., no tag in XML response) rather
1103                         // than "" when the corresponding request
1104                         // field was empty or nil.
1105                         c.Check(result.StartAfter, check.DeepEquals, stringOrNil(trial.startAfter))
1106                         c.Check(result.ContinuationToken, check.DeepEquals, params.ContinuationToken)
1107
1108                         if trial.maxKeys > 0 {
1109                                 c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(trial.maxKeys)))
1110                                 c.Check(len(result.Contents)+len(result.CommonPrefixes) <= trial.maxKeys, check.Equals, true)
1111                         } else {
1112                                 c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(s3MaxKeys)))
1113                         }
1114
1115                         for _, ent := range result.Contents {
1116                                 c.Assert(ent.Key, check.NotNil)
1117                                 c.Check(*ent.Key > trial.startAfter, check.Equals, true)
1118                                 c.Check(keySeen[*ent.Key], check.Equals, false, check.Commentf("dup key %q", *ent.Key))
1119                                 keySeen[*ent.Key] = true
1120                         }
1121                         for _, ent := range result.CommonPrefixes {
1122                                 c.Assert(ent.Prefix, check.NotNil)
1123                                 c.Check(strings.HasSuffix(*ent.Prefix, trial.delimiter), check.Equals, true, check.Commentf("bad CommonPrefix %q", *ent.Prefix))
1124                                 if strings.HasPrefix(trial.startAfter, *ent.Prefix) {
1125                                         // If we asked for
1126                                         // startAfter=dir0/file10.txt,
1127                                         // we expect dir0/ to be
1128                                         // returned as a common prefix
1129                                 } else {
1130                                         c.Check(*ent.Prefix > trial.startAfter, check.Equals, true)
1131                                 }
1132                                 c.Check(prefixSeen[*ent.Prefix], check.Equals, false, check.Commentf("dup common prefix %q", *ent.Prefix))
1133                                 prefixSeen[*ent.Prefix] = true
1134                         }
1135                         if *result.IsTruncated && c.Check(result.NextContinuationToken, check.Not(check.Equals), "") {
1136                                 params.ContinuationToken = aws_aws.String(*result.NextContinuationToken)
1137                         } else {
1138                                 break
1139                         }
1140                 }
1141                 c.Check(keySeen, check.HasLen, trial.expectKeys)
1142                 c.Check(prefixSeen, check.HasLen, len(trial.expectCommonPrefixes))
1143                 if len(trial.expectCommonPrefixes) > 0 {
1144                         c.Check(prefixSeen, check.DeepEquals, trial.expectCommonPrefixes)
1145                 }
1146         }
1147 }
1148
1149 func (s *IntegrationSuite) TestS3ListObjectsV2EncodingTypeURL(c *check.C) {
1150         stage := s.s3setup(c)
1151         defer stage.teardown(c)
1152         dirs := 2
1153         filesPerDir := 40
1154         stage.writeBigDirs(c, dirs, filesPerDir)
1155
1156         sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
1157                 Region:           aws_aws.String("auto"),
1158                 Endpoint:         aws_aws.String(s.testServer.URL),
1159                 Credentials:      aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
1160                 S3ForcePathStyle: aws_aws.Bool(true),
1161         }))
1162
1163         client := aws_s3.New(sess)
1164         ctx := context.Background()
1165
1166         result, err := client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
1167                 Bucket:       aws_aws.String(stage.collbucket.Name),
1168                 Prefix:       aws_aws.String("dir0/"),
1169                 Delimiter:    aws_aws.String("/"),
1170                 StartAfter:   aws_aws.String("dir0/"),
1171                 EncodingType: aws_aws.String("url"),
1172         })
1173         c.Assert(err, check.IsNil)
1174         c.Check(*result.Prefix, check.Equals, "dir0%2F")
1175         c.Check(*result.Delimiter, check.Equals, "%2F")
1176         c.Check(*result.StartAfter, check.Equals, "dir0%2F")
1177         for _, ent := range result.Contents {
1178                 c.Check(*ent.Key, check.Matches, "dir0%2F.*")
1179         }
1180         result, err = client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
1181                 Bucket:       aws_aws.String(stage.collbucket.Name),
1182                 Delimiter:    aws_aws.String("/"),
1183                 EncodingType: aws_aws.String("url"),
1184         })
1185         c.Assert(err, check.IsNil)
1186         c.Check(*result.Delimiter, check.Equals, "%2F")
1187         c.Check(result.CommonPrefixes, check.HasLen, dirs+1)
1188         for _, ent := range result.CommonPrefixes {
1189                 c.Check(*ent.Prefix, check.Matches, ".*%2F")
1190         }
1191 }
1192
1193 // TestS3cmd checks compatibility with the s3cmd command line tool, if
1194 // it's installed. As of Debian buster, s3cmd is only in backports, so
1195 // `arvados-server install` don't install it, and this test skips if
1196 // it's not installed.
1197 func (s *IntegrationSuite) TestS3cmd(c *check.C) {
1198         if _, err := exec.LookPath("s3cmd"); err != nil {
1199                 c.Skip("s3cmd not found")
1200                 return
1201         }
1202
1203         stage := s.s3setup(c)
1204         defer stage.teardown(c)
1205
1206         cmd := exec.Command("s3cmd", "--no-ssl", "--host="+s.testServer.URL[7:], "--host-bucket="+s.testServer.URL[7:], "--access_key="+arvadostest.ActiveTokenUUID, "--secret_key="+arvadostest.ActiveToken, "ls", "s3://"+arvadostest.FooCollection)
1207         buf, err := cmd.CombinedOutput()
1208         c.Check(err, check.IsNil)
1209         c.Check(string(buf), check.Matches, `.* 3 +s3://`+arvadostest.FooCollection+`/foo\n`)
1210
1211         // This tests whether s3cmd's path normalization agrees with
1212         // keep-web's signature verification wrt chars like "|"
1213         // (neither reserved nor unreserved) and "," (not normally
1214         // percent-encoded in a path).
1215         tmpfile := c.MkDir() + "/dstfile"
1216         cmd = exec.Command("s3cmd", "--no-ssl", "--host="+s.testServer.URL[7:], "--host-bucket="+s.testServer.URL[7:], "--access_key="+arvadostest.ActiveTokenUUID, "--secret_key="+arvadostest.ActiveToken, "get", "s3://"+arvadostest.FooCollection+"/foo,;$[|]bar", tmpfile)
1217         buf, err = cmd.CombinedOutput()
1218         c.Check(err, check.NotNil)
1219         c.Check(string(buf), check.Matches, `(?ms).*NoSuchKey.*\n`)
1220 }
1221
1222 func (s *IntegrationSuite) TestS3BucketInHost(c *check.C) {
1223         stage := s.s3setup(c)
1224         defer stage.teardown(c)
1225
1226         hdr, body, _ := s.runCurl(c, "AWS "+arvadostest.ActiveTokenV2+":none", stage.coll.UUID+".collections.example.com", "/sailboat.txt")
1227         c.Check(hdr, check.Matches, `(?s)HTTP/1.1 200 OK\r\n.*`)
1228         c.Check(body, check.Equals, "⛵\n")
1229 }