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