19362: Use hardlinks to a singleton for each project/collection.
[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, 404)
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, "", http.Header{
397                         "Authorization": {"Bearer " + arvadostest.ActiveToken},
398                 })
399                 c.Check(resp.Code, check.Equals, http.StatusOK)
400         }
401 }
402
403 func (s *IntegrationSuite) TestS3ProjectPutObjectNotSupported(c *check.C) {
404         stage := s.s3setup(c)
405         defer stage.teardown(c)
406         bucket := stage.projbucket
407
408         for _, trial := range []struct {
409                 path        string
410                 size        int
411                 contentType string
412         }{
413                 {
414                         path:        "newfile",
415                         size:        1234,
416                         contentType: "application/octet-stream",
417                 }, {
418                         path:        "newdir/newfile",
419                         size:        1234,
420                         contentType: "application/octet-stream",
421                 }, {
422                         path:        "newdir2/",
423                         size:        0,
424                         contentType: "application/x-directory",
425                 },
426         } {
427                 c.Logf("=== %v", trial)
428
429                 _, err := bucket.GetReader(trial.path)
430                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
431                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
432                 c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
433
434                 buf := make([]byte, trial.size)
435                 rand.Read(buf)
436
437                 err = bucket.PutReader(trial.path, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
438                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 400)
439                 c.Check(err.(*s3.Error).Code, check.Equals, `InvalidArgument`)
440                 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)`)
441
442                 _, err = bucket.GetReader(trial.path)
443                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
444                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
445                 c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
446         }
447 }
448
449 func (s *IntegrationSuite) TestS3CollectionDeleteObject(c *check.C) {
450         stage := s.s3setup(c)
451         defer stage.teardown(c)
452         s.testS3DeleteObject(c, stage.collbucket, "")
453 }
454 func (s *IntegrationSuite) TestS3ProjectDeleteObject(c *check.C) {
455         stage := s.s3setup(c)
456         defer stage.teardown(c)
457         s.testS3DeleteObject(c, stage.projbucket, stage.coll.Name+"/")
458 }
459 func (s *IntegrationSuite) testS3DeleteObject(c *check.C, bucket *s3.Bucket, prefix string) {
460         s.handler.Cluster.Collections.S3FolderObjects = true
461         for _, trial := range []struct {
462                 path string
463         }{
464                 {"/"},
465                 {"nonexistentfile"},
466                 {"emptyfile"},
467                 {"sailboat.txt"},
468                 {"sailboat.txt/"},
469                 {"emptydir"},
470                 {"emptydir/"},
471         } {
472                 objname := prefix + trial.path
473                 comment := check.Commentf("objname %q", objname)
474
475                 err := bucket.Del(objname)
476                 if trial.path == "/" {
477                         c.Check(err, check.NotNil)
478                         continue
479                 }
480                 c.Check(err, check.IsNil, comment)
481                 _, err = bucket.GetReader(objname)
482                 c.Check(err, check.NotNil, comment)
483         }
484 }
485
486 func (s *IntegrationSuite) TestS3CollectionPutObjectFailure(c *check.C) {
487         stage := s.s3setup(c)
488         defer stage.teardown(c)
489         s.testS3PutObjectFailure(c, stage.collbucket, "")
490 }
491 func (s *IntegrationSuite) TestS3ProjectPutObjectFailure(c *check.C) {
492         stage := s.s3setup(c)
493         defer stage.teardown(c)
494         s.testS3PutObjectFailure(c, stage.projbucket, stage.coll.Name+"/")
495 }
496 func (s *IntegrationSuite) testS3PutObjectFailure(c *check.C, bucket *s3.Bucket, prefix string) {
497         s.handler.Cluster.Collections.S3FolderObjects = false
498
499         var wg sync.WaitGroup
500         for _, trial := range []struct {
501                 path string
502         }{
503                 {
504                         path: "emptyfile/newname", // emptyfile exists, see s3setup()
505                 }, {
506                         path: "emptyfile/", // emptyfile exists, see s3setup()
507                 }, {
508                         path: "emptydir", // dir already exists, see s3setup()
509                 }, {
510                         path: "emptydir/",
511                 }, {
512                         path: "emptydir//",
513                 }, {
514                         path: "newdir/",
515                 }, {
516                         path: "newdir//",
517                 }, {
518                         path: "/",
519                 }, {
520                         path: "//",
521                 }, {
522                         path: "",
523                 },
524         } {
525                 trial := trial
526                 wg.Add(1)
527                 go func() {
528                         defer wg.Done()
529                         c.Logf("=== %v", trial)
530
531                         objname := prefix + trial.path
532
533                         buf := make([]byte, 1234)
534                         rand.Read(buf)
535
536                         err := bucket.PutReader(objname, bytes.NewReader(buf), int64(len(buf)), "application/octet-stream", s3.Private, s3.Options{})
537                         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)) {
538                                 return
539                         }
540
541                         if objname != "" && objname != "/" {
542                                 _, err = bucket.GetReader(objname)
543                                 c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
544                                 c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
545                                 c.Check(err, check.ErrorMatches, `The specified key does not exist.`, check.Commentf("GET %q should return 404", objname))
546                         }
547                 }()
548         }
549         wg.Wait()
550 }
551
552 func (stage *s3stage) writeBigDirs(c *check.C, dirs int, filesPerDir int) {
553         fs, err := stage.coll.FileSystem(stage.arv, stage.kc)
554         c.Assert(err, check.IsNil)
555         for d := 0; d < dirs; d++ {
556                 dir := fmt.Sprintf("dir%d", d)
557                 c.Assert(fs.Mkdir(dir, 0755), check.IsNil)
558                 for i := 0; i < filesPerDir; i++ {
559                         f, err := fs.OpenFile(fmt.Sprintf("%s/file%d.txt", dir, i), os.O_CREATE|os.O_WRONLY, 0644)
560                         c.Assert(err, check.IsNil)
561                         c.Assert(f.Close(), check.IsNil)
562                 }
563         }
564         c.Assert(fs.Sync(), check.IsNil)
565 }
566
567 func (s *IntegrationSuite) sign(c *check.C, req *http.Request, key, secret string) {
568         scope := "20200202/zzzzz/service/aws4_request"
569         signedHeaders := "date"
570         req.Header.Set("Date", time.Now().UTC().Format(time.RFC1123))
571         stringToSign, err := s3stringToSign(s3SignAlgorithm, scope, signedHeaders, req)
572         c.Assert(err, check.IsNil)
573         sig, err := s3signature(secret, scope, signedHeaders, stringToSign)
574         c.Assert(err, check.IsNil)
575         req.Header.Set("Authorization", s3SignAlgorithm+" Credential="+key+"/"+scope+", SignedHeaders="+signedHeaders+", Signature="+sig)
576 }
577
578 func (s *IntegrationSuite) TestS3VirtualHostStyleRequests(c *check.C) {
579         stage := s.s3setup(c)
580         defer stage.teardown(c)
581         for _, trial := range []struct {
582                 url            string
583                 method         string
584                 body           string
585                 responseCode   int
586                 responseRegexp []string
587         }{
588                 {
589                         url:            "https://" + stage.collbucket.Name + ".example.com/",
590                         method:         "GET",
591                         responseCode:   http.StatusOK,
592                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
593                 },
594                 {
595                         url:            "https://" + strings.Replace(stage.coll.PortableDataHash, "+", "-", -1) + ".example.com/",
596                         method:         "GET",
597                         responseCode:   http.StatusOK,
598                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
599                 },
600                 {
601                         url:            "https://" + stage.projbucket.Name + ".example.com/?prefix=" + stage.coll.Name + "/&delimiter=/",
602                         method:         "GET",
603                         responseCode:   http.StatusOK,
604                         responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
605                 },
606                 {
607                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/sailboat.txt",
608                         method:         "GET",
609                         responseCode:   http.StatusOK,
610                         responseRegexp: []string{`⛵\n`},
611                 },
612                 {
613                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
614                         method:       "PUT",
615                         body:         "boop",
616                         responseCode: http.StatusOK,
617                 },
618                 {
619                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
620                         method:         "GET",
621                         responseCode:   http.StatusOK,
622                         responseRegexp: []string{`boop`},
623                 },
624                 {
625                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
626                         method:       "GET",
627                         responseCode: http.StatusNotFound,
628                 },
629                 {
630                         url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
631                         method:       "PUT",
632                         body:         "boop",
633                         responseCode: http.StatusOK,
634                 },
635                 {
636                         url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "//boop",
637                         method:         "GET",
638                         responseCode:   http.StatusOK,
639                         responseRegexp: []string{`boop`},
640                 },
641         } {
642                 url, err := url.Parse(trial.url)
643                 c.Assert(err, check.IsNil)
644                 req, err := http.NewRequest(trial.method, url.String(), bytes.NewReader([]byte(trial.body)))
645                 c.Assert(err, check.IsNil)
646                 s.sign(c, req, arvadostest.ActiveTokenUUID, arvadostest.ActiveToken)
647                 rr := httptest.NewRecorder()
648                 s.handler.ServeHTTP(rr, req)
649                 resp := rr.Result()
650                 c.Check(resp.StatusCode, check.Equals, trial.responseCode)
651                 body, err := ioutil.ReadAll(resp.Body)
652                 c.Assert(err, check.IsNil)
653                 for _, re := range trial.responseRegexp {
654                         c.Check(string(body), check.Matches, re)
655                 }
656         }
657 }
658
659 func (s *IntegrationSuite) TestS3NormalizeURIForSignature(c *check.C) {
660         stage := s.s3setup(c)
661         defer stage.teardown(c)
662         for _, trial := range []struct {
663                 rawPath        string
664                 normalizedPath string
665         }{
666                 {"/foo", "/foo"},                           // boring case
667                 {"/foo%5fbar", "/foo_bar"},                 // _ must not be escaped
668                 {"/foo%2fbar", "/foo/bar"},                 // / must not be escaped
669                 {"/(foo)/[];,", "/%28foo%29/%5B%5D%3B%2C"}, // ()[];, must be escaped
670                 {"/foo%5bbar", "/foo%5Bbar"},               // %XX must be uppercase
671                 {"//foo///.bar", "/foo/.bar"},              // "//" and "///" must be squashed to "/"
672         } {
673                 c.Logf("trial %q", trial)
674
675                 date := time.Now().UTC().Format("20060102T150405Z")
676                 scope := "20200202/zzzzz/S3/aws4_request"
677                 canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", "GET", trial.normalizedPath, "", "host:host.example.com\n", "host", "")
678                 c.Logf("canonicalRequest %q", canonicalRequest)
679                 expect := fmt.Sprintf("%s\n%s\n%s\n%s", s3SignAlgorithm, date, scope, hashdigest(sha256.New(), canonicalRequest))
680                 c.Logf("expected stringToSign %q", expect)
681
682                 req, err := http.NewRequest("GET", "https://host.example.com"+trial.rawPath, nil)
683                 req.Header.Set("X-Amz-Date", date)
684                 req.Host = "host.example.com"
685                 c.Assert(err, check.IsNil)
686
687                 obtained, err := s3stringToSign(s3SignAlgorithm, scope, "host", req)
688                 if !c.Check(err, check.IsNil) {
689                         continue
690                 }
691                 c.Check(obtained, check.Equals, expect)
692         }
693 }
694
695 func (s *IntegrationSuite) TestS3GetBucketLocation(c *check.C) {
696         stage := s.s3setup(c)
697         defer stage.teardown(c)
698         for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
699                 req, err := http.NewRequest("GET", bucket.URL("/"), nil)
700                 c.Check(err, check.IsNil)
701                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
702                 req.URL.RawQuery = "location"
703                 resp, err := http.DefaultClient.Do(req)
704                 c.Assert(err, check.IsNil)
705                 c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
706                 buf, err := ioutil.ReadAll(resp.Body)
707                 c.Assert(err, check.IsNil)
708                 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")
709         }
710 }
711
712 func (s *IntegrationSuite) TestS3GetBucketVersioning(c *check.C) {
713         stage := s.s3setup(c)
714         defer stage.teardown(c)
715         for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
716                 req, err := http.NewRequest("GET", bucket.URL("/"), nil)
717                 c.Check(err, check.IsNil)
718                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
719                 req.URL.RawQuery = "versioning"
720                 resp, err := http.DefaultClient.Do(req)
721                 c.Assert(err, check.IsNil)
722                 c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
723                 buf, err := ioutil.ReadAll(resp.Body)
724                 c.Assert(err, check.IsNil)
725                 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")
726         }
727 }
728
729 func (s *IntegrationSuite) TestS3UnsupportedAPIs(c *check.C) {
730         stage := s.s3setup(c)
731         defer stage.teardown(c)
732         for _, trial := range []struct {
733                 method   string
734                 path     string
735                 rawquery string
736         }{
737                 {"GET", "/", "acl&versionId=1234"},    // GetBucketAcl
738                 {"GET", "/foo", "acl&versionId=1234"}, // GetObjectAcl
739                 {"PUT", "/", "acl"},                   // PutBucketAcl
740                 {"PUT", "/foo", "acl"},                // PutObjectAcl
741                 {"DELETE", "/", "tagging"},            // DeleteBucketTagging
742                 {"DELETE", "/foo", "tagging"},         // DeleteObjectTagging
743         } {
744                 for _, bucket := range []*s3.Bucket{stage.collbucket, stage.projbucket} {
745                         c.Logf("trial %v bucket %v", trial, bucket)
746                         req, err := http.NewRequest(trial.method, bucket.URL(trial.path), nil)
747                         c.Check(err, check.IsNil)
748                         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
749                         req.URL.RawQuery = trial.rawquery
750                         resp, err := http.DefaultClient.Do(req)
751                         c.Assert(err, check.IsNil)
752                         c.Check(resp.Header.Get("Content-Type"), check.Equals, "application/xml")
753                         buf, err := ioutil.ReadAll(resp.Body)
754                         c.Assert(err, check.IsNil)
755                         c.Check(string(buf), check.Matches, "(?ms).*InvalidRequest.*API not supported.*")
756                 }
757         }
758 }
759
760 // If there are no CommonPrefixes entries, the CommonPrefixes XML tag
761 // should not appear at all.
762 func (s *IntegrationSuite) TestS3ListNoCommonPrefixes(c *check.C) {
763         stage := s.s3setup(c)
764         defer stage.teardown(c)
765
766         req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
767         c.Assert(err, check.IsNil)
768         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
769         req.URL.RawQuery = "prefix=asdfasdfasdf&delimiter=/"
770         resp, err := http.DefaultClient.Do(req)
771         c.Assert(err, check.IsNil)
772         buf, err := ioutil.ReadAll(resp.Body)
773         c.Assert(err, check.IsNil)
774         c.Check(string(buf), check.Not(check.Matches), `(?ms).*CommonPrefixes.*`)
775 }
776
777 // If there is no delimiter in the request, or the results are not
778 // truncated, the NextMarker XML tag should not appear in the response
779 // body.
780 func (s *IntegrationSuite) TestS3ListNoNextMarker(c *check.C) {
781         stage := s.s3setup(c)
782         defer stage.teardown(c)
783
784         for _, query := range []string{"prefix=e&delimiter=/", ""} {
785                 req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
786                 c.Assert(err, check.IsNil)
787                 req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
788                 req.URL.RawQuery = query
789                 resp, err := http.DefaultClient.Do(req)
790                 c.Assert(err, check.IsNil)
791                 buf, err := ioutil.ReadAll(resp.Body)
792                 c.Assert(err, check.IsNil)
793                 c.Check(string(buf), check.Not(check.Matches), `(?ms).*NextMarker.*`)
794         }
795 }
796
797 // List response should include KeyCount field.
798 func (s *IntegrationSuite) TestS3ListKeyCount(c *check.C) {
799         stage := s.s3setup(c)
800         defer stage.teardown(c)
801
802         req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
803         c.Assert(err, check.IsNil)
804         req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
805         req.URL.RawQuery = "prefix=&delimiter=/"
806         resp, err := http.DefaultClient.Do(req)
807         c.Assert(err, check.IsNil)
808         buf, err := ioutil.ReadAll(resp.Body)
809         c.Assert(err, check.IsNil)
810         c.Check(string(buf), check.Matches, `(?ms).*<KeyCount>2</KeyCount>.*`)
811 }
812
813 func (s *IntegrationSuite) TestS3CollectionList(c *check.C) {
814         stage := s.s3setup(c)
815         defer stage.teardown(c)
816
817         var markers int
818         for markers, s.handler.Cluster.Collections.S3FolderObjects = range []bool{false, true} {
819                 dirs := 2
820                 filesPerDir := 1001
821                 stage.writeBigDirs(c, dirs, filesPerDir)
822                 // Total # objects is:
823                 //                 2 file entries from s3setup (emptyfile and sailboat.txt)
824                 //                +1 fake "directory" marker from s3setup (emptydir) (if enabled)
825                 //             +dirs fake "directory" marker from writeBigDirs (dir0/, dir1/) (if enabled)
826                 // +filesPerDir*dirs file entries from writeBigDirs (dir0/file0.txt, etc.)
827                 s.testS3List(c, stage.collbucket, "", 4000, markers+2+(filesPerDir+markers)*dirs)
828                 s.testS3List(c, stage.collbucket, "", 131, markers+2+(filesPerDir+markers)*dirs)
829                 s.testS3List(c, stage.collbucket, "dir0/", 71, filesPerDir+markers)
830         }
831 }
832 func (s *IntegrationSuite) testS3List(c *check.C, bucket *s3.Bucket, prefix string, pageSize, expectFiles int) {
833         c.Logf("testS3List: prefix=%q pageSize=%d S3FolderObjects=%v", prefix, pageSize, s.handler.Cluster.Collections.S3FolderObjects)
834         expectPageSize := pageSize
835         if expectPageSize > 1000 {
836                 expectPageSize = 1000
837         }
838         gotKeys := map[string]s3.Key{}
839         nextMarker := ""
840         pages := 0
841         for {
842                 resp, err := bucket.List(prefix, "", nextMarker, pageSize)
843                 if !c.Check(err, check.IsNil) {
844                         break
845                 }
846                 c.Check(len(resp.Contents) <= expectPageSize, check.Equals, true)
847                 if pages++; !c.Check(pages <= (expectFiles/expectPageSize)+1, check.Equals, true) {
848                         break
849                 }
850                 for _, key := range resp.Contents {
851                         gotKeys[key.Key] = key
852                         if strings.Contains(key.Key, "sailboat.txt") {
853                                 c.Check(key.Size, check.Equals, int64(4))
854                         }
855                 }
856                 if !resp.IsTruncated {
857                         c.Check(resp.NextMarker, check.Equals, "")
858                         break
859                 }
860                 if !c.Check(resp.NextMarker, check.Not(check.Equals), "") {
861                         break
862                 }
863                 nextMarker = resp.NextMarker
864         }
865         c.Check(len(gotKeys), check.Equals, expectFiles)
866 }
867
868 func (s *IntegrationSuite) TestS3CollectionListRollup(c *check.C) {
869         for _, s.handler.Cluster.Collections.S3FolderObjects = range []bool{false, true} {
870                 s.testS3CollectionListRollup(c)
871         }
872 }
873
874 func (s *IntegrationSuite) testS3CollectionListRollup(c *check.C) {
875         stage := s.s3setup(c)
876         defer stage.teardown(c)
877
878         dirs := 2
879         filesPerDir := 500
880         stage.writeBigDirs(c, dirs, filesPerDir)
881         err := stage.collbucket.PutReader("dingbats", &bytes.Buffer{}, 0, "application/octet-stream", s3.Private, s3.Options{})
882         c.Assert(err, check.IsNil)
883         var allfiles []string
884         for marker := ""; ; {
885                 resp, err := stage.collbucket.List("", "", marker, 20000)
886                 c.Check(err, check.IsNil)
887                 for _, key := range resp.Contents {
888                         if len(allfiles) == 0 || allfiles[len(allfiles)-1] != key.Key {
889                                 allfiles = append(allfiles, key.Key)
890                         }
891                 }
892                 marker = resp.NextMarker
893                 if marker == "" {
894                         break
895                 }
896         }
897         markers := 0
898         if s.handler.Cluster.Collections.S3FolderObjects {
899                 markers = 1
900         }
901         c.Check(allfiles, check.HasLen, dirs*(filesPerDir+markers)+3+markers)
902
903         gotDirMarker := map[string]bool{}
904         for _, name := range allfiles {
905                 isDirMarker := strings.HasSuffix(name, "/")
906                 if markers == 0 {
907                         c.Check(isDirMarker, check.Equals, false, check.Commentf("name %q", name))
908                 } else if isDirMarker {
909                         gotDirMarker[name] = true
910                 } else if i := strings.LastIndex(name, "/"); i >= 0 {
911                         c.Check(gotDirMarker[name[:i+1]], check.Equals, true, check.Commentf("name %q", name))
912                         gotDirMarker[name[:i+1]] = true // skip redundant complaints about this dir marker
913                 }
914         }
915
916         for _, trial := range []struct {
917                 prefix    string
918                 delimiter string
919                 marker    string
920         }{
921                 {"", "", ""},
922                 {"di", "/", ""},
923                 {"di", "r", ""},
924                 {"di", "n", ""},
925                 {"dir0", "/", ""},
926                 {"dir0/", "/", ""},
927                 {"dir0/f", "/", ""},
928                 {"dir0", "", ""},
929                 {"dir0/", "", ""},
930                 {"dir0/f", "", ""},
931                 {"dir0", "/", "dir0/file14.txt"},       // no commonprefixes
932                 {"", "", "dir0/file14.txt"},            // middle page, skip walking dir1
933                 {"", "", "dir1/file14.txt"},            // middle page, skip walking dir0
934                 {"", "", "dir1/file498.txt"},           // last page of results
935                 {"dir1/file", "", "dir1/file498.txt"},  // last page of results, with prefix
936                 {"dir1/file", "/", "dir1/file498.txt"}, // last page of results, with prefix + delimiter
937                 {"dir1", "Z", "dir1/file498.txt"},      // delimiter "Z" never appears
938                 {"dir2", "/", ""},                      // prefix "dir2" does not exist
939                 {"", "/", ""},
940         } {
941                 c.Logf("\n\n=== trial %+v markers=%d", trial, markers)
942
943                 maxKeys := 20
944                 resp, err := stage.collbucket.List(trial.prefix, trial.delimiter, trial.marker, maxKeys)
945                 c.Check(err, check.IsNil)
946                 if resp.IsTruncated && trial.delimiter == "" {
947                         // goamz List method fills in the missing
948                         // NextMarker field if resp.IsTruncated, so
949                         // now we can't really tell whether it was
950                         // sent by the server or by goamz. In cases
951                         // where it should be empty but isn't, assume
952                         // it's goamz's fault.
953                         resp.NextMarker = ""
954                 }
955
956                 var expectKeys []string
957                 var expectPrefixes []string
958                 var expectNextMarker string
959                 var expectTruncated bool
960                 for _, key := range allfiles {
961                         full := len(expectKeys)+len(expectPrefixes) >= maxKeys
962                         if !strings.HasPrefix(key, trial.prefix) || key < trial.marker {
963                                 continue
964                         } else if idx := strings.Index(key[len(trial.prefix):], trial.delimiter); trial.delimiter != "" && idx >= 0 {
965                                 prefix := key[:len(trial.prefix)+idx+1]
966                                 if len(expectPrefixes) > 0 && expectPrefixes[len(expectPrefixes)-1] == prefix {
967                                         // same prefix as previous key
968                                 } else if full {
969                                         expectNextMarker = key
970                                         expectTruncated = true
971                                 } else {
972                                         expectPrefixes = append(expectPrefixes, prefix)
973                                 }
974                         } else if full {
975                                 if trial.delimiter != "" {
976                                         expectNextMarker = key
977                                 }
978                                 expectTruncated = true
979                                 break
980                         } else {
981                                 expectKeys = append(expectKeys, key)
982                         }
983                 }
984
985                 var gotKeys []string
986                 for _, key := range resp.Contents {
987                         gotKeys = append(gotKeys, key.Key)
988                 }
989                 var gotPrefixes []string
990                 for _, prefix := range resp.CommonPrefixes {
991                         gotPrefixes = append(gotPrefixes, prefix)
992                 }
993                 commentf := check.Commentf("trial %+v markers=%d", trial, markers)
994                 c.Check(gotKeys, check.DeepEquals, expectKeys, commentf)
995                 c.Check(gotPrefixes, check.DeepEquals, expectPrefixes, commentf)
996                 c.Check(resp.NextMarker, check.Equals, expectNextMarker, commentf)
997                 c.Check(resp.IsTruncated, check.Equals, expectTruncated, commentf)
998                 c.Logf("=== trial %+v keys %q prefixes %q nextMarker %q", trial, gotKeys, gotPrefixes, resp.NextMarker)
999         }
1000 }
1001
1002 func (s *IntegrationSuite) TestS3ListObjectsV2(c *check.C) {
1003         stage := s.s3setup(c)
1004         defer stage.teardown(c)
1005         dirs := 2
1006         filesPerDir := 40
1007         stage.writeBigDirs(c, dirs, filesPerDir)
1008
1009         sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
1010                 Region:           aws_aws.String("auto"),
1011                 Endpoint:         aws_aws.String(s.testServer.URL),
1012                 Credentials:      aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
1013                 S3ForcePathStyle: aws_aws.Bool(true),
1014         }))
1015
1016         stringOrNil := func(s string) *string {
1017                 if s == "" {
1018                         return nil
1019                 } else {
1020                         return &s
1021                 }
1022         }
1023
1024         client := aws_s3.New(sess)
1025         ctx := context.Background()
1026
1027         for _, trial := range []struct {
1028                 prefix               string
1029                 delimiter            string
1030                 startAfter           string
1031                 maxKeys              int
1032                 expectKeys           int
1033                 expectCommonPrefixes map[string]bool
1034         }{
1035                 {
1036                         // Expect {filesPerDir plus the dir itself}
1037                         // for each dir, plus emptydir, emptyfile, and
1038                         // sailboat.txt.
1039                         expectKeys: (filesPerDir+1)*dirs + 3,
1040                 },
1041                 {
1042                         maxKeys:    15,
1043                         expectKeys: (filesPerDir+1)*dirs + 3,
1044                 },
1045                 {
1046                         startAfter: "dir0/z",
1047                         maxKeys:    15,
1048                         // Expect {filesPerDir plus the dir itself}
1049                         // for each dir except dir0, plus emptydir,
1050                         // emptyfile, and sailboat.txt.
1051                         expectKeys: (filesPerDir+1)*(dirs-1) + 3,
1052                 },
1053                 {
1054                         maxKeys:              1,
1055                         delimiter:            "/",
1056                         expectKeys:           2, // emptyfile, sailboat.txt
1057                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
1058                 },
1059                 {
1060                         startAfter:           "dir0/z",
1061                         maxKeys:              15,
1062                         delimiter:            "/",
1063                         expectKeys:           2, // emptyfile, sailboat.txt
1064                         expectCommonPrefixes: map[string]bool{"dir1/": true, "emptydir/": true},
1065                 },
1066                 {
1067                         startAfter:           "dir0/file10.txt",
1068                         maxKeys:              15,
1069                         delimiter:            "/",
1070                         expectKeys:           2,
1071                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
1072                 },
1073                 {
1074                         startAfter:           "dir0/file10.txt",
1075                         maxKeys:              15,
1076                         prefix:               "d",
1077                         delimiter:            "/",
1078                         expectKeys:           0,
1079                         expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true},
1080                 },
1081         } {
1082                 c.Logf("[trial %+v]", trial)
1083                 params := aws_s3.ListObjectsV2Input{
1084                         Bucket:     aws_aws.String(stage.collbucket.Name),
1085                         Prefix:     stringOrNil(trial.prefix),
1086                         Delimiter:  stringOrNil(trial.delimiter),
1087                         StartAfter: stringOrNil(trial.startAfter),
1088                         MaxKeys:    aws_aws.Int64(int64(trial.maxKeys)),
1089                 }
1090                 keySeen := map[string]bool{}
1091                 prefixSeen := map[string]bool{}
1092                 for {
1093                         result, err := client.ListObjectsV2WithContext(ctx, &params)
1094                         if !c.Check(err, check.IsNil) {
1095                                 break
1096                         }
1097                         c.Check(result.Name, check.DeepEquals, aws_aws.String(stage.collbucket.Name))
1098                         c.Check(result.Prefix, check.DeepEquals, aws_aws.String(trial.prefix))
1099                         c.Check(result.Delimiter, check.DeepEquals, aws_aws.String(trial.delimiter))
1100                         // The following two fields are expected to be
1101                         // nil (i.e., no tag in XML response) rather
1102                         // than "" when the corresponding request
1103                         // field was empty or nil.
1104                         c.Check(result.StartAfter, check.DeepEquals, stringOrNil(trial.startAfter))
1105                         c.Check(result.ContinuationToken, check.DeepEquals, params.ContinuationToken)
1106
1107                         if trial.maxKeys > 0 {
1108                                 c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(trial.maxKeys)))
1109                                 c.Check(len(result.Contents)+len(result.CommonPrefixes) <= trial.maxKeys, check.Equals, true)
1110                         } else {
1111                                 c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(s3MaxKeys)))
1112                         }
1113
1114                         for _, ent := range result.Contents {
1115                                 c.Assert(ent.Key, check.NotNil)
1116                                 c.Check(*ent.Key > trial.startAfter, check.Equals, true)
1117                                 c.Check(keySeen[*ent.Key], check.Equals, false, check.Commentf("dup key %q", *ent.Key))
1118                                 keySeen[*ent.Key] = true
1119                         }
1120                         for _, ent := range result.CommonPrefixes {
1121                                 c.Assert(ent.Prefix, check.NotNil)
1122                                 c.Check(strings.HasSuffix(*ent.Prefix, trial.delimiter), check.Equals, true, check.Commentf("bad CommonPrefix %q", *ent.Prefix))
1123                                 if strings.HasPrefix(trial.startAfter, *ent.Prefix) {
1124                                         // If we asked for
1125                                         // startAfter=dir0/file10.txt,
1126                                         // we expect dir0/ to be
1127                                         // returned as a common prefix
1128                                 } else {
1129                                         c.Check(*ent.Prefix > trial.startAfter, check.Equals, true)
1130                                 }
1131                                 c.Check(prefixSeen[*ent.Prefix], check.Equals, false, check.Commentf("dup common prefix %q", *ent.Prefix))
1132                                 prefixSeen[*ent.Prefix] = true
1133                         }
1134                         if *result.IsTruncated && c.Check(result.NextContinuationToken, check.Not(check.Equals), "") {
1135                                 params.ContinuationToken = aws_aws.String(*result.NextContinuationToken)
1136                         } else {
1137                                 break
1138                         }
1139                 }
1140                 c.Check(keySeen, check.HasLen, trial.expectKeys)
1141                 c.Check(prefixSeen, check.HasLen, len(trial.expectCommonPrefixes))
1142                 if len(trial.expectCommonPrefixes) > 0 {
1143                         c.Check(prefixSeen, check.DeepEquals, trial.expectCommonPrefixes)
1144                 }
1145         }
1146 }
1147
1148 func (s *IntegrationSuite) TestS3ListObjectsV2EncodingTypeURL(c *check.C) {
1149         stage := s.s3setup(c)
1150         defer stage.teardown(c)
1151         dirs := 2
1152         filesPerDir := 40
1153         stage.writeBigDirs(c, dirs, filesPerDir)
1154
1155         sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
1156                 Region:           aws_aws.String("auto"),
1157                 Endpoint:         aws_aws.String(s.testServer.URL),
1158                 Credentials:      aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
1159                 S3ForcePathStyle: aws_aws.Bool(true),
1160         }))
1161
1162         client := aws_s3.New(sess)
1163         ctx := context.Background()
1164
1165         result, err := client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
1166                 Bucket:       aws_aws.String(stage.collbucket.Name),
1167                 Prefix:       aws_aws.String("dir0/"),
1168                 Delimiter:    aws_aws.String("/"),
1169                 StartAfter:   aws_aws.String("dir0/"),
1170                 EncodingType: aws_aws.String("url"),
1171         })
1172         c.Assert(err, check.IsNil)
1173         c.Check(*result.Prefix, check.Equals, "dir0%2F")
1174         c.Check(*result.Delimiter, check.Equals, "%2F")
1175         c.Check(*result.StartAfter, check.Equals, "dir0%2F")
1176         for _, ent := range result.Contents {
1177                 c.Check(*ent.Key, check.Matches, "dir0%2F.*")
1178         }
1179         result, err = client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
1180                 Bucket:       aws_aws.String(stage.collbucket.Name),
1181                 Delimiter:    aws_aws.String("/"),
1182                 EncodingType: aws_aws.String("url"),
1183         })
1184         c.Assert(err, check.IsNil)
1185         c.Check(*result.Delimiter, check.Equals, "%2F")
1186         c.Check(result.CommonPrefixes, check.HasLen, dirs+1)
1187         for _, ent := range result.CommonPrefixes {
1188                 c.Check(*ent.Prefix, check.Matches, ".*%2F")
1189         }
1190 }
1191
1192 // TestS3cmd checks compatibility with the s3cmd command line tool, if
1193 // it's installed. As of Debian buster, s3cmd is only in backports, so
1194 // `arvados-server install` don't install it, and this test skips if
1195 // it's not installed.
1196 func (s *IntegrationSuite) TestS3cmd(c *check.C) {
1197         if _, err := exec.LookPath("s3cmd"); err != nil {
1198                 c.Skip("s3cmd not found")
1199                 return
1200         }
1201
1202         stage := s.s3setup(c)
1203         defer stage.teardown(c)
1204
1205         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)
1206         buf, err := cmd.CombinedOutput()
1207         c.Check(err, check.IsNil)
1208         c.Check(string(buf), check.Matches, `.* 3 +s3://`+arvadostest.FooCollection+`/foo\n`)
1209
1210         // This tests whether s3cmd's path normalization agrees with
1211         // keep-web's signature verification wrt chars like "|"
1212         // (neither reserved nor unreserved) and "," (not normally
1213         // percent-encoded in a path).
1214         tmpfile := c.MkDir() + "/dstfile"
1215         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)
1216         buf, err = cmd.CombinedOutput()
1217         c.Check(err, check.NotNil)
1218         c.Check(string(buf), check.Matches, `(?ms).*NoSuchKey.*\n`)
1219 }
1220
1221 func (s *IntegrationSuite) TestS3BucketInHost(c *check.C) {
1222         stage := s.s3setup(c)
1223         defer stage.teardown(c)
1224
1225         hdr, body, _ := s.runCurl(c, "AWS "+arvadostest.ActiveTokenV2+":none", stage.coll.UUID+".collections.example.com", "/sailboat.txt")
1226         c.Check(hdr, check.Matches, `(?s)HTTP/1.1 200 OK\r\n.*`)
1227         c.Check(body, check.Equals, "⛵\n")
1228 }