1/*
2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
3 * Copyright 2015-2017 MinIO, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package minio
19
20import (
21	"context"
22	"net"
23	"net/http"
24	"net/url"
25	"path"
26	"sync"
27
28	"github.com/minio/minio-go/v7/pkg/credentials"
29	"github.com/minio/minio-go/v7/pkg/s3utils"
30	"github.com/minio/minio-go/v7/pkg/signer"
31)
32
33// bucketLocationCache - Provides simple mechanism to hold bucket
34// locations in memory.
35type bucketLocationCache struct {
36	// mutex is used for handling the concurrent
37	// read/write requests for cache.
38	sync.RWMutex
39
40	// items holds the cached bucket locations.
41	items map[string]string
42}
43
44// newBucketLocationCache - Provides a new bucket location cache to be
45// used internally with the client object.
46func newBucketLocationCache() *bucketLocationCache {
47	return &bucketLocationCache{
48		items: make(map[string]string),
49	}
50}
51
52// Get - Returns a value of a given key if it exists.
53func (r *bucketLocationCache) Get(bucketName string) (location string, ok bool) {
54	r.RLock()
55	defer r.RUnlock()
56	location, ok = r.items[bucketName]
57	return
58}
59
60// Set - Will persist a value into cache.
61func (r *bucketLocationCache) Set(bucketName string, location string) {
62	r.Lock()
63	defer r.Unlock()
64	r.items[bucketName] = location
65}
66
67// Delete - Deletes a bucket name from cache.
68func (r *bucketLocationCache) Delete(bucketName string) {
69	r.Lock()
70	defer r.Unlock()
71	delete(r.items, bucketName)
72}
73
74// GetBucketLocation - get location for the bucket name from location cache, if not
75// fetch freshly by making a new request.
76func (c Client) GetBucketLocation(ctx context.Context, bucketName string) (string, error) {
77	if err := s3utils.CheckValidBucketName(bucketName); err != nil {
78		return "", err
79	}
80	return c.getBucketLocation(ctx, bucketName)
81}
82
83// getBucketLocation - Get location for the bucketName from location map cache, if not
84// fetch freshly by making a new request.
85func (c Client) getBucketLocation(ctx context.Context, bucketName string) (string, error) {
86	if err := s3utils.CheckValidBucketName(bucketName); err != nil {
87		return "", err
88	}
89
90	// Region set then no need to fetch bucket location.
91	if c.region != "" {
92		return c.region, nil
93	}
94
95	if location, ok := c.bucketLocCache.Get(bucketName); ok {
96		return location, nil
97	}
98
99	// Initialize a new request.
100	req, err := c.getBucketLocationRequest(ctx, bucketName)
101	if err != nil {
102		return "", err
103	}
104
105	// Initiate the request.
106	resp, err := c.do(req)
107	defer closeResponse(resp)
108	if err != nil {
109		return "", err
110	}
111	location, err := processBucketLocationResponse(resp, bucketName)
112	if err != nil {
113		return "", err
114	}
115	c.bucketLocCache.Set(bucketName, location)
116	return location, nil
117}
118
119// processes the getBucketLocation http response from the server.
120func processBucketLocationResponse(resp *http.Response, bucketName string) (bucketLocation string, err error) {
121	if resp != nil {
122		if resp.StatusCode != http.StatusOK {
123			err = httpRespToErrorResponse(resp, bucketName, "")
124			errResp := ToErrorResponse(err)
125			// For access denied error, it could be an anonymous
126			// request. Move forward and let the top level callers
127			// succeed if possible based on their policy.
128			switch errResp.Code {
129			case "NotImplemented":
130				if errResp.Server == "AmazonSnowball" {
131					return "snowball", nil
132				}
133			case "AuthorizationHeaderMalformed":
134				fallthrough
135			case "InvalidRegion":
136				fallthrough
137			case "AccessDenied":
138				if errResp.Region == "" {
139					return "us-east-1", nil
140				}
141				return errResp.Region, nil
142			}
143			return "", err
144		}
145	}
146
147	// Extract location.
148	var locationConstraint string
149	err = xmlDecoder(resp.Body, &locationConstraint)
150	if err != nil {
151		return "", err
152	}
153
154	location := locationConstraint
155	// Location is empty will be 'us-east-1'.
156	if location == "" {
157		location = "us-east-1"
158	}
159
160	// Location can be 'EU' convert it to meaningful 'eu-west-1'.
161	if location == "EU" {
162		location = "eu-west-1"
163	}
164
165	// Save the location into cache.
166
167	// Return.
168	return location, nil
169}
170
171// getBucketLocationRequest - Wrapper creates a new getBucketLocation request.
172func (c Client) getBucketLocationRequest(ctx context.Context, bucketName string) (*http.Request, error) {
173	// Set location query.
174	urlValues := make(url.Values)
175	urlValues.Set("location", "")
176
177	// Set get bucket location always as path style.
178	targetURL := *c.endpointURL
179
180	// as it works in makeTargetURL method from api.go file
181	if h, p, err := net.SplitHostPort(targetURL.Host); err == nil {
182		if targetURL.Scheme == "http" && p == "80" || targetURL.Scheme == "https" && p == "443" {
183			targetURL.Host = h
184		}
185	}
186
187	isVirtualHost := s3utils.IsVirtualHostSupported(targetURL, bucketName)
188
189	var urlStr string
190
191	//only support Aliyun OSS for virtual hosted path,  compatible  Amazon & Google Endpoint
192	if isVirtualHost && s3utils.IsAliyunOSSEndpoint(targetURL) {
193		urlStr = c.endpointURL.Scheme + "://" + bucketName + "." + targetURL.Host + "/?location"
194	} else {
195		targetURL.Path = path.Join(bucketName, "") + "/"
196		targetURL.RawQuery = urlValues.Encode()
197		urlStr = targetURL.String()
198	}
199
200	// Get a new HTTP request for the method.
201	req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
202	if err != nil {
203		return nil, err
204	}
205
206	// Set UserAgent for the request.
207	c.setUserAgent(req)
208
209	// Get credentials from the configured credentials provider.
210	value, err := c.credsProvider.Get()
211	if err != nil {
212		return nil, err
213	}
214
215	var (
216		signerType      = value.SignerType
217		accessKeyID     = value.AccessKeyID
218		secretAccessKey = value.SecretAccessKey
219		sessionToken    = value.SessionToken
220	)
221
222	// Custom signer set then override the behavior.
223	if c.overrideSignerType != credentials.SignatureDefault {
224		signerType = c.overrideSignerType
225	}
226
227	// If signerType returned by credentials helper is anonymous,
228	// then do not sign regardless of signerType override.
229	if value.SignerType == credentials.SignatureAnonymous {
230		signerType = credentials.SignatureAnonymous
231	}
232
233	if signerType.IsAnonymous() {
234		return req, nil
235	}
236
237	if signerType.IsV2() {
238		// Get Bucket Location calls should be always path style
239		isVirtualHost := false
240		req = signer.SignV2(*req, accessKeyID, secretAccessKey, isVirtualHost)
241		return req, nil
242	}
243
244	// Set sha256 sum for signature calculation only with signature version '4'.
245	contentSha256 := emptySHA256Hex
246	if c.secure {
247		contentSha256 = unsignedPayload
248	}
249
250	req.Header.Set("X-Amz-Content-Sha256", contentSha256)
251	req = signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, "us-east-1")
252	return req, nil
253}
254