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  }