1// Copyright (c) 2015-2021 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 18package cmd 19 20// bucketHandler is an http.Handler that verifies bucket responses and validates incoming requests 21import ( 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 . "gopkg.in/check.v1" 31) 32 33type bucketHandler struct { 34 resource string 35} 36 37func (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 77type objectHandler struct { 78 resource string 79 data []byte 80} 81 82func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 83 switch { 84 case r.Method == "PUT": 85 // Handler for PUT object request. 86 length, e := strconv.Atoi(r.Header.Get("Content-Length")) 87 if e != nil { 88 w.WriteHeader(http.StatusBadRequest) 89 return 90 } 91 var buffer bytes.Buffer 92 if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil { 93 w.WriteHeader(http.StatusInternalServerError) 94 return 95 } 96 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 97 w.WriteHeader(http.StatusOK) 98 case r.Method == "HEAD": 99 // Handler for Stat object request. 100 if r.URL.Path != h.resource { 101 w.WriteHeader(http.StatusNotFound) 102 return 103 } 104 w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) 105 w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) 106 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 107 w.WriteHeader(http.StatusOK) 108 case r.Method == "POST": 109 // Handler for multipart upload request. 110 if _, ok := r.URL.Query()["uploads"]; ok { 111 if r.URL.Path == h.resource { 112 response := []byte("<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>bucket</Bucket><Key>object</Key><UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId></InitiateMultipartUploadResult>") 113 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 114 w.Write(response) 115 return 116 } 117 } 118 if _, ok := r.URL.Query()["uploadId"]; ok { 119 if r.URL.Path == h.resource { 120 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>") 121 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 122 w.Write(response) 123 return 124 } 125 } 126 if r.URL.Path != h.resource { 127 w.WriteHeader(http.StatusNotFound) 128 return 129 } 130 case r.Method == "GET": 131 // Handler for get bucket location request. 132 if _, ok := r.URL.Query()["location"]; ok { 133 response := []byte("<LocationConstraint xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"></LocationConstraint>") 134 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 135 w.Write(response) 136 return 137 } 138 // Handler for list multipart upload request. 139 if _, ok := r.URL.Query()["uploads"]; ok { 140 if r.URL.Path == "/bucket/" { 141 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>") 142 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 143 w.Write(response) 144 return 145 } 146 } 147 if r.URL.Path != h.resource { 148 w.WriteHeader(http.StatusNotFound) 149 return 150 } 151 w.Header().Set("Content-Length", strconv.Itoa(len(h.data))) 152 w.Header().Set("Last-Modified", UTCNow().Format(http.TimeFormat)) 153 w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe") 154 w.WriteHeader(http.StatusOK) 155 io.Copy(w, bytes.NewReader(h.data)) 156 } 157} 158 159// Test bucket operations. 160func (s *TestSuite) TestBucketOperations(c *C) { 161 bucket := bucketHandler{ 162 resource: "/bucket/", 163 } 164 server := httptest.NewServer(bucket) 165 defer server.Close() 166 167 conf := new(Config) 168 conf.HostURL = server.URL + bucket.resource 169 conf.AccessKey = "WLGDGYAQYIGI833EV05A" 170 conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" 171 conf.Signature = "S3v4" 172 s3c, err := S3New(conf) 173 c.Assert(err, IsNil) 174 175 err = s3c.MakeBucket(context.Background(), "us-east-1", true, false) 176 c.Assert(err, IsNil) 177 178 conf.HostURL = server.URL + string(s3c.GetURL().Separator) 179 s3c, err = S3New(conf) 180 c.Assert(err, IsNil) 181 182 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 183 c.Assert(content.Err, IsNil) 184 c.Assert(content.Type.IsDir(), Equals, true) 185 } 186 187 conf.HostURL = server.URL + "/bucket" 188 s3c, err = S3New(conf) 189 c.Assert(err, IsNil) 190 191 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 192 c.Assert(content.Err, IsNil) 193 c.Assert(content.Type.IsDir(), Equals, true) 194 } 195 196 conf.HostURL = server.URL + "/bucket/" 197 s3c, err = S3New(conf) 198 c.Assert(err, IsNil) 199 200 for content := range s3c.List(globalContext, ListOptions{ShowDir: DirNone}) { 201 c.Assert(content.Err, IsNil) 202 c.Assert(content.Type.IsRegular(), Equals, true) 203 } 204} 205 206// Test all object operations. 207func (s *TestSuite) TestObjectOperations(c *C) { 208 object := objectHandler{ 209 resource: "/bucket/object", 210 data: []byte("Hello, World"), 211 } 212 server := httptest.NewServer(object) 213 defer server.Close() 214 215 conf := new(Config) 216 conf.HostURL = server.URL + object.resource 217 conf.AccessKey = "WLGDGYAQYIGI833EV05A" 218 conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF" 219 conf.Signature = "S3v4" 220 s3c, err := S3New(conf) 221 c.Assert(err, IsNil) 222 223 var reader io.Reader 224 reader = bytes.NewReader(object.data) 225 n, err := s3c.Put(context.Background(), reader, int64(len(object.data)), nil, PutOptions{ 226 metadata: map[string]string{ 227 "Content-Type": "application/octet-stream", 228 }, 229 }) 230 c.Assert(err, IsNil) 231 c.Assert(n, Equals, int64(len(object.data))) 232 233 reader, err = s3c.Get(context.Background(), GetOptions{}) 234 c.Assert(err, IsNil) 235 var buffer bytes.Buffer 236 { 237 _, err := io.Copy(&buffer, reader) 238 c.Assert(err, IsNil) 239 c.Assert(buffer.Bytes(), DeepEquals, object.data) 240 } 241} 242 243var testSelectCompressionTypeCases = []struct { 244 opts SelectObjectOpts 245 object string 246 compressionType minio.SelectCompressionType 247}{ 248 {SelectObjectOpts{CompressionType: minio.SelectCompressionNONE}, "a.gzip", minio.SelectCompressionNONE}, 249 {SelectObjectOpts{CompressionType: minio.SelectCompressionBZIP}, "a.gz", minio.SelectCompressionBZIP}, 250 {SelectObjectOpts{}, "t.parquet", minio.SelectCompressionNONE}, 251 {SelectObjectOpts{}, "x.csv.gz", minio.SelectCompressionGZIP}, 252 {SelectObjectOpts{}, "x.json.bz2", minio.SelectCompressionBZIP}, 253 {SelectObjectOpts{}, "b.gz", minio.SelectCompressionGZIP}, 254 {SelectObjectOpts{}, "k.bz2", minio.SelectCompressionBZIP}, 255 {SelectObjectOpts{}, "a.csv", minio.SelectCompressionNONE}, 256 {SelectObjectOpts{}, "a.json", minio.SelectCompressionNONE}, 257} 258 259// TestSelectCompressionType - tests compression type returned 260// by method 261func (s *TestSuite) TestSelectCompressionType(c *C) { 262 for _, test := range testSelectCompressionTypeCases { 263 cType := selectCompressionType(test.opts, test.object) 264 c.Assert(cType, DeepEquals, test.compressionType) 265 } 266} 267