github.com/minio/mc@v0.0.0-20240507152021-646712d5e5fb/cmd/client-s3_test.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 // bucketHandler is an http.Handler that verifies bucket responses and validates incoming requests 21 import ( 22 "bytes" 23 "context" 24 "io" 25 "net/http" 26 "net/http/httptest" 27 "strconv" 28 29 minio "github.com/minio/minio-go/v7" 30 checkv1 "gopkg.in/check.v1" 31 ) 32 33 type bucketHandler struct { 34 resource string 35 } 36 37 func (h bucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 38 switch { 39 case r.Method == "GET": 40 // Handler for incoming getBucketLocation request. 41 if _, ok := r.URL.Query()["location"]; ok { 42 response := []byte("<LocationConstraint xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"></LocationConstraint>") 43 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 44 w.Write(response) 45 return 46 } 47 switch { 48 case r.URL.Path == "/": 49 // Handler for incoming ListBuckets request. 50 response := []byte("<ListAllMyBucketsResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Buckets><Bucket><Name>bucket</Name><CreationDate>2015-05-20T23:05:09.230Z</CreationDate></Bucket></Buckets><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner></ListAllMyBucketsResult>") 51 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 52 w.Write(response) 53 case r.URL.Path == "/bucket/": 54 // Handler for incoming ListObjects request. 55 response := []byte("<ListBucketResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Contents><ETag>259d04a13802ae09c7e41be50ccc6baa</ETag><Key>object</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Delimiter></Delimiter><EncodingType></EncodingType><IsTruncated>false</IsTruncated><Marker></Marker><MaxKeys>1000</MaxKeys><Name>testbucket</Name><NextMarker></NextMarker><Prefix></Prefix></ListBucketResult>") 56 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 57 w.Write(response) 58 } 59 case r.Method == "PUT": 60 switch { 61 case r.URL.Path == h.resource: 62 w.WriteHeader(http.StatusOK) 63 default: 64 w.WriteHeader(http.StatusBadRequest) 65 } 66 case r.Method == "HEAD": 67 switch { 68 case r.URL.Path == h.resource: 69 w.WriteHeader(http.StatusOK) 70 default: 71 w.WriteHeader(http.StatusForbidden) 72 } 73 } 74 } 75 76 // objectHandler is an http.Handler that verifies object responses and validates incoming requests 77 type objectHandler struct { 78 resource string 79 data []byte 80 } 81 82 func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 83 if ak := r.Header.Get("Authorization"); len(ak) == 0 { 84 w.WriteHeader(http.StatusForbidden) 85 return 86 } 87 88 switch { 89 case r.Method == http.MethodPut: 90 // Handler for PUT object request. 91 length, e := strconv.Atoi(r.Header.Get("Content-Length")) 92 if e != nil { 93 w.WriteHeader(http.StatusBadRequest) 94 return 95 } 96 var buffer bytes.Buffer 97 if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil { 98 w.WriteHeader(http.StatusInternalServerError) 99 return 100 } 101 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 102 w.WriteHeader(http.StatusOK) 103 case r.Method == http.MethodHead: 104 // Handler for Stat object request. 105 if r.URL.Path != h.resource { 106 w.WriteHeader(http.StatusNotFound) 107 return 108 } 109 w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) 110 w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) 111 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 112 w.WriteHeader(http.StatusOK) 113 case r.Method == http.MethodPost: 114 // Handler for multipart upload request. 115 if _, ok := r.URL.Query()["uploads"]; ok { 116 if r.URL.Path == h.resource { 117 response := []byte("<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>bucket</Bucket><Key>object</Key><UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId></InitiateMultipartUploadResult>") 118 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 119 w.Write(response) 120 return 121 } 122 } 123 if _, ok := r.URL.Query()["uploadId"]; ok { 124 if r.URL.Path == h.resource { 125 response := []byte("<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Location>http://bucket.s3.amazonaws.com/object</Location><Bucket>bucket</Bucket><Key>object</Key><ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag></CompleteMultipartUploadResult>") 126 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 127 w.Write(response) 128 return 129 } 130 } 131 if r.URL.Path != h.resource { 132 w.WriteHeader(http.StatusNotFound) 133 return 134 } 135 case r.Method == http.MethodGet: 136 // Handler for get bucket location request. 137 if _, ok := r.URL.Query()["location"]; ok { 138 response := []byte("<LocationConstraint xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"></LocationConstraint>") 139 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 140 w.Write(response) 141 return 142 } 143 // Handler for list multipart upload request. 144 if _, ok := r.URL.Query()["uploads"]; ok { 145 if r.URL.Path == "/bucket/" { 146 response := []byte("<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>bucket</Bucket><KeyMarker/><UploadIdMarker/><NextKeyMarker/><NextUploadIdMarker/><EncodingType/><MaxUploads>1000</MaxUploads><IsTruncated>false</IsTruncated><Prefix/><Delimiter/></ListMultipartUploadsResult>") 147 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 148 w.Write(response) 149 return 150 } 151 } 152 if r.URL.Path != h.resource { 153 w.WriteHeader(http.StatusNotFound) 154 return 155 } 156 w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) 157 w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) 158 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 159 w.WriteHeader(http.StatusOK) 160 io.Copy(w, bytes.NewReader(h.data)) 161 } 162 } 163 164 type stsHandler struct { 165 endpoint string 166 jwt []byte 167 } 168 169 func (h stsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 170 if err := ParseForm(r); err != nil { 171 w.WriteHeader(http.StatusInternalServerError) 172 return 173 } 174 switch { 175 case r.Method == http.MethodPost: 176 token := r.Form.Get("WebIdentityToken") 177 if token == string(h.jwt) { 178 response := []byte("<AssumeRoleWithWebIdentityResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\"><AssumeRoleWithWebIdentityResult><AssumedRoleUser><Arn></Arn><AssumeRoleId></AssumeRoleId></AssumedRoleUser><Credentials><AccessKeyId>7NL5BR739GUQ0ZOD4JNB</AccessKeyId><SecretAccessKey>A2mxZSxPnHNhSduedUHczsXZpVSSssOLpDruUmTV</SecretAccessKey><Expiration>0001-01-01T00:00:00Z</Expiration><SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI3Tkw1QlI3MzlHVVEwWk9ENEpOQiIsImV4cCI6MTY5OTYwMzMwNiwicGFyZW50IjoibWluaW8iLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaVlXUnRhVzQ2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbXR0Y3pvcUlsMTlMSHNpUldabVpXTjBJam9pUVd4c2IzY2lMQ0pCWTNScGIyNGlPbHNpY3pNNktpSmRMQ0pTWlhOdmRYSmpaU0k2V3lKaGNtNDZZWGR6T25Nek9qbzZLaUpkZlYxOSJ9.uuE_x7PO8QoPfUk9KzUELoAqxihIknZAvJLl5aYJjwpSjJYFTPLp6EvuyJX2hc18s9HzeiJ-vU0dPzsy50dXmg</SessionToken></Credentials></AssumeRoleWithWebIdentityResult><ResponseMetadata></ResponseMetadata></AssumeRoleWithWebIdentityResponse>") 179 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 180 w.Header().Set("Content-Type", "application/xml") 181 w.Header().Set("Server", "MinIO") 182 w.Write(response) 183 return 184 } else { 185 response := []byte("<ErrorResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\"><Error><Type></Type><Code>AccessDenied</Code><Message>Access denied: Invalid Token</Message></Error><RequestId></RequestId></ErrorResponse>") 186 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 187 w.Header().Set("Content-Type", "application/xml") 188 w.Write(response) 189 return 190 } 191 } 192 } 193 194 // Test bucket operations. 195 func (s *TestSuite) TestBucketOperations(c *checkv1.C) { 196 bucket := bucketHandler{ 197 resource: "/bucket/", 198 } 199 server := httptest.NewServer(bucket) 200 defer server.Close() 201 202 conf := new(Config) 203 conf.HostURL = server.URL + bucket.resource 204 conf.AccessKey = "WLGDGYAQYIGI833EV05A" 205 conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" 206 conf.Signature = "S3v4" 207 s3c, err := S3New(conf) 208 c.Assert(err, checkv1.IsNil) 209 210 err = s3c.MakeBucket(context.Background(), "us-east-1", true, false) 211 c.Assert(err, checkv1.IsNil) 212 213 conf.HostURL = server.URL + string(s3c.GetURL().Separator) 214 s3c, err = S3New(conf) 215 c.Assert(err, checkv1.IsNil) 216 217 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 218 c.Assert(content.Err, checkv1.IsNil) 219 c.Assert(content.Type.IsDir(), checkv1.Equals, true) 220 } 221 222 conf.HostURL = server.URL + "/bucket" 223 s3c, err = S3New(conf) 224 c.Assert(err, checkv1.IsNil) 225 226 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 227 c.Assert(content.Err, checkv1.IsNil) 228 c.Assert(content.Type.IsDir(), checkv1.Equals, true) 229 } 230 231 conf.HostURL = server.URL + "/bucket/" 232 s3c, err = S3New(conf) 233 c.Assert(err, checkv1.IsNil) 234 235 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 236 c.Assert(content.Err, checkv1.IsNil) 237 c.Assert(content.Type.IsRegular(), checkv1.Equals, true) 238 } 239 } 240 241 // Test all object operations. 242 func (s *TestSuite) TestObjectOperations(c *checkv1.C) { 243 object := objectHandler{ 244 resource: "/bucket/object", 245 data: []byte("Hello, World"), 246 } 247 server := httptest.NewServer(object) 248 defer server.Close() 249 250 conf := new(Config) 251 conf.HostURL = server.URL + object.resource 252 conf.AccessKey = "WLGDGYAQYIGI833EV05A" 253 conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" 254 conf.Signature = "S3v4" 255 s3c, err := S3New(conf) 256 c.Assert(err, checkv1.IsNil) 257 258 var reader io.Reader 259 reader = bytes.NewReader(object.data) 260 n, err := s3c.Put(context.Background(), reader, int64(len(object.data)), nil, PutOptions{ 261 metadata: map[string]string{ 262 "Content-Type": "application/octet-stream", 263 }, 264 }) 265 c.Assert(err, checkv1.IsNil) 266 c.Assert(n, checkv1.Equals, int64(len(object.data))) 267 268 reader, _, err = s3c.Get(context.Background(), GetOptions{}) 269 c.Assert(err, checkv1.IsNil) 270 var buffer bytes.Buffer 271 { 272 _, err := io.Copy(&buffer, reader) 273 c.Assert(err, checkv1.IsNil) 274 c.Assert(buffer.Bytes(), checkv1.DeepEquals, object.data) 275 } 276 } 277 278 var testSelectCompressionTypeCases = []struct { 279 opts SelectObjectOpts 280 object string 281 compressionType minio.SelectCompressionType 282 }{ 283 {SelectObjectOpts{CompressionType: minio.SelectCompressionNONE}, "a.gzip", minio.SelectCompressionNONE}, 284 {SelectObjectOpts{CompressionType: minio.SelectCompressionBZIP}, "a.gz", minio.SelectCompressionBZIP}, 285 {SelectObjectOpts{}, "t.parquet", minio.SelectCompressionNONE}, 286 {SelectObjectOpts{}, "x.csv.gz", minio.SelectCompressionGZIP}, 287 {SelectObjectOpts{}, "x.json.bz2", minio.SelectCompressionBZIP}, 288 {SelectObjectOpts{}, "b.gz", minio.SelectCompressionGZIP}, 289 {SelectObjectOpts{}, "k.bz2", minio.SelectCompressionBZIP}, 290 {SelectObjectOpts{}, "a.csv", minio.SelectCompressionNONE}, 291 {SelectObjectOpts{}, "a.json", minio.SelectCompressionNONE}, 292 } 293 294 // TestSelectCompressionType - tests compression type returned 295 // by method 296 func (s *TestSuite) TestSelectCompressionType(c *checkv1.C) { 297 for _, test := range testSelectCompressionTypeCases { 298 cType := selectCompressionType(test.opts, test.object) 299 c.Assert(cType, checkv1.DeepEquals, test.compressionType) 300 } 301 }