diff options
Diffstat (limited to 'vendor/github.com/minio/minio-go/v7/pkg/s3utils')
| -rw-r--r-- | vendor/github.com/minio/minio-go/v7/pkg/s3utils/utils.go | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/vendor/github.com/minio/minio-go/v7/pkg/s3utils/utils.go b/vendor/github.com/minio/minio-go/v7/pkg/s3utils/utils.go new file mode 100644 index 0000000..056e78a --- /dev/null +++ b/vendor/github.com/minio/minio-go/v7/pkg/s3utils/utils.go | |||
| @@ -0,0 +1,411 @@ | |||
| 1 | /* | ||
| 2 | * MinIO Go Library for Amazon S3 Compatible Cloud Storage | ||
| 3 | * Copyright 2015-2020 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 | |||
| 18 | package s3utils | ||
| 19 | |||
| 20 | import ( | ||
| 21 | "bytes" | ||
| 22 | "encoding/hex" | ||
| 23 | "errors" | ||
| 24 | "net" | ||
| 25 | "net/url" | ||
| 26 | "regexp" | ||
| 27 | "sort" | ||
| 28 | "strings" | ||
| 29 | "unicode/utf8" | ||
| 30 | ) | ||
| 31 | |||
| 32 | // Sentinel URL is the default url value which is invalid. | ||
| 33 | var sentinelURL = url.URL{} | ||
| 34 | |||
| 35 | // IsValidDomain validates if input string is a valid domain name. | ||
| 36 | func IsValidDomain(host string) bool { | ||
| 37 | // See RFC 1035, RFC 3696. | ||
| 38 | host = strings.TrimSpace(host) | ||
| 39 | if len(host) == 0 || len(host) > 255 { | ||
| 40 | return false | ||
| 41 | } | ||
| 42 | // host cannot start or end with "-" | ||
| 43 | if host[len(host)-1:] == "-" || host[:1] == "-" { | ||
| 44 | return false | ||
| 45 | } | ||
| 46 | // host cannot start or end with "_" | ||
| 47 | if host[len(host)-1:] == "_" || host[:1] == "_" { | ||
| 48 | return false | ||
| 49 | } | ||
| 50 | // host cannot start with a "." | ||
| 51 | if host[:1] == "." { | ||
| 52 | return false | ||
| 53 | } | ||
| 54 | // All non alphanumeric characters are invalid. | ||
| 55 | if strings.ContainsAny(host, "`~!@#$%^&*()+={}[]|\\\"';:><?/") { | ||
| 56 | return false | ||
| 57 | } | ||
| 58 | // No need to regexp match, since the list is non-exhaustive. | ||
| 59 | // We let it valid and fail later. | ||
| 60 | return true | ||
| 61 | } | ||
| 62 | |||
| 63 | // IsValidIP parses input string for ip address validity. | ||
| 64 | func IsValidIP(ip string) bool { | ||
| 65 | return net.ParseIP(ip) != nil | ||
| 66 | } | ||
| 67 | |||
| 68 | // IsVirtualHostSupported - verifies if bucketName can be part of | ||
| 69 | // virtual host. Currently only Amazon S3 and Google Cloud Storage | ||
| 70 | // would support this. | ||
| 71 | func IsVirtualHostSupported(endpointURL url.URL, bucketName string) bool { | ||
| 72 | if endpointURL == sentinelURL { | ||
| 73 | return false | ||
| 74 | } | ||
| 75 | // bucketName can be valid but '.' in the hostname will fail SSL | ||
| 76 | // certificate validation. So do not use host-style for such buckets. | ||
| 77 | if endpointURL.Scheme == "https" && strings.Contains(bucketName, ".") { | ||
| 78 | return false | ||
| 79 | } | ||
| 80 | // Return true for all other cases | ||
| 81 | return IsAmazonEndpoint(endpointURL) || IsGoogleEndpoint(endpointURL) || IsAliyunOSSEndpoint(endpointURL) | ||
| 82 | } | ||
| 83 | |||
| 84 | // Refer for region styles - https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region | ||
| 85 | |||
| 86 | // amazonS3HostHyphen - regular expression used to determine if an arg is s3 host in hyphenated style. | ||
| 87 | var amazonS3HostHyphen = regexp.MustCompile(`^s3-(.*?).amazonaws.com$`) | ||
| 88 | |||
| 89 | // amazonS3HostDualStack - regular expression used to determine if an arg is s3 host dualstack. | ||
| 90 | var amazonS3HostDualStack = regexp.MustCompile(`^s3.dualstack.(.*?).amazonaws.com$`) | ||
| 91 | |||
| 92 | // amazonS3HostFIPS - regular expression used to determine if an arg is s3 FIPS host. | ||
| 93 | var amazonS3HostFIPS = regexp.MustCompile(`^s3-fips.(.*?).amazonaws.com$`) | ||
| 94 | |||
| 95 | // amazonS3HostFIPSDualStack - regular expression used to determine if an arg is s3 FIPS host dualstack. | ||
| 96 | var amazonS3HostFIPSDualStack = regexp.MustCompile(`^s3-fips.dualstack.(.*?).amazonaws.com$`) | ||
| 97 | |||
| 98 | // amazonS3HostDot - regular expression used to determine if an arg is s3 host in . style. | ||
| 99 | var amazonS3HostDot = regexp.MustCompile(`^s3.(.*?).amazonaws.com$`) | ||
| 100 | |||
| 101 | // amazonS3ChinaHost - regular expression used to determine if the arg is s3 china host. | ||
| 102 | var amazonS3ChinaHost = regexp.MustCompile(`^s3.(cn.*?).amazonaws.com.cn$`) | ||
| 103 | |||
| 104 | // amazonS3ChinaHostDualStack - regular expression used to determine if the arg is s3 china host dualstack. | ||
| 105 | var amazonS3ChinaHostDualStack = regexp.MustCompile(`^s3.dualstack.(cn.*?).amazonaws.com.cn$`) | ||
| 106 | |||
| 107 | // Regular expression used to determine if the arg is elb host. | ||
| 108 | var elbAmazonRegex = regexp.MustCompile(`elb(.*?).amazonaws.com$`) | ||
| 109 | |||
| 110 | // Regular expression used to determine if the arg is elb host in china. | ||
| 111 | var elbAmazonCnRegex = regexp.MustCompile(`elb(.*?).amazonaws.com.cn$`) | ||
| 112 | |||
| 113 | // amazonS3HostPrivateLink - regular expression used to determine if an arg is s3 host in AWS PrivateLink interface endpoints style | ||
| 114 | var amazonS3HostPrivateLink = regexp.MustCompile(`^(?:bucket|accesspoint).vpce-.*?.s3.(.*?).vpce.amazonaws.com$`) | ||
| 115 | |||
| 116 | // GetRegionFromURL - returns a region from url host. | ||
| 117 | func GetRegionFromURL(endpointURL url.URL) string { | ||
| 118 | if endpointURL == sentinelURL { | ||
| 119 | return "" | ||
| 120 | } | ||
| 121 | if endpointURL.Host == "s3-external-1.amazonaws.com" { | ||
| 122 | return "" | ||
| 123 | } | ||
| 124 | |||
| 125 | // if elb's are used we cannot calculate which region it may be, just return empty. | ||
| 126 | if elbAmazonRegex.MatchString(endpointURL.Host) || elbAmazonCnRegex.MatchString(endpointURL.Host) { | ||
| 127 | return "" | ||
| 128 | } | ||
| 129 | |||
| 130 | // We check for FIPS dualstack matching first to avoid the non-greedy | ||
| 131 | // regex for FIPS non-dualstack matching a dualstack URL | ||
| 132 | parts := amazonS3HostFIPSDualStack.FindStringSubmatch(endpointURL.Host) | ||
| 133 | if len(parts) > 1 { | ||
| 134 | return parts[1] | ||
| 135 | } | ||
| 136 | |||
| 137 | parts = amazonS3HostFIPS.FindStringSubmatch(endpointURL.Host) | ||
| 138 | if len(parts) > 1 { | ||
| 139 | return parts[1] | ||
| 140 | } | ||
| 141 | |||
| 142 | parts = amazonS3HostDualStack.FindStringSubmatch(endpointURL.Host) | ||
| 143 | if len(parts) > 1 { | ||
| 144 | return parts[1] | ||
| 145 | } | ||
| 146 | |||
| 147 | parts = amazonS3HostHyphen.FindStringSubmatch(endpointURL.Host) | ||
| 148 | if len(parts) > 1 { | ||
| 149 | return parts[1] | ||
| 150 | } | ||
| 151 | |||
| 152 | parts = amazonS3ChinaHost.FindStringSubmatch(endpointURL.Host) | ||
| 153 | if len(parts) > 1 { | ||
| 154 | return parts[1] | ||
| 155 | } | ||
| 156 | |||
| 157 | parts = amazonS3ChinaHostDualStack.FindStringSubmatch(endpointURL.Host) | ||
| 158 | if len(parts) > 1 { | ||
| 159 | return parts[1] | ||
| 160 | } | ||
| 161 | |||
| 162 | parts = amazonS3HostDot.FindStringSubmatch(endpointURL.Host) | ||
| 163 | if len(parts) > 1 { | ||
| 164 | return parts[1] | ||
| 165 | } | ||
| 166 | |||
| 167 | parts = amazonS3HostPrivateLink.FindStringSubmatch(endpointURL.Host) | ||
| 168 | if len(parts) > 1 { | ||
| 169 | return parts[1] | ||
| 170 | } | ||
| 171 | |||
| 172 | return "" | ||
| 173 | } | ||
| 174 | |||
| 175 | // IsAliyunOSSEndpoint - Match if it is exactly Aliyun OSS endpoint. | ||
| 176 | func IsAliyunOSSEndpoint(endpointURL url.URL) bool { | ||
| 177 | return strings.HasSuffix(endpointURL.Host, "aliyuncs.com") | ||
| 178 | } | ||
| 179 | |||
| 180 | // IsAmazonEndpoint - Match if it is exactly Amazon S3 endpoint. | ||
| 181 | func IsAmazonEndpoint(endpointURL url.URL) bool { | ||
| 182 | if endpointURL.Host == "s3-external-1.amazonaws.com" || endpointURL.Host == "s3.amazonaws.com" { | ||
| 183 | return true | ||
| 184 | } | ||
| 185 | return GetRegionFromURL(endpointURL) != "" | ||
| 186 | } | ||
| 187 | |||
| 188 | // IsAmazonGovCloudEndpoint - Match if it is exactly Amazon S3 GovCloud endpoint. | ||
| 189 | func IsAmazonGovCloudEndpoint(endpointURL url.URL) bool { | ||
| 190 | if endpointURL == sentinelURL { | ||
| 191 | return false | ||
| 192 | } | ||
| 193 | return (endpointURL.Host == "s3-us-gov-west-1.amazonaws.com" || | ||
| 194 | endpointURL.Host == "s3-us-gov-east-1.amazonaws.com" || | ||
| 195 | IsAmazonFIPSGovCloudEndpoint(endpointURL)) | ||
| 196 | } | ||
| 197 | |||
| 198 | // IsAmazonFIPSGovCloudEndpoint - match if the endpoint is FIPS and GovCloud. | ||
| 199 | func IsAmazonFIPSGovCloudEndpoint(endpointURL url.URL) bool { | ||
| 200 | if endpointURL == sentinelURL { | ||
| 201 | return false | ||
| 202 | } | ||
| 203 | return IsAmazonFIPSEndpoint(endpointURL) && strings.Contains(endpointURL.Host, "us-gov-") | ||
| 204 | } | ||
| 205 | |||
| 206 | // IsAmazonFIPSEndpoint - Match if it is exactly Amazon S3 FIPS endpoint. | ||
| 207 | // See https://aws.amazon.com/compliance/fips. | ||
| 208 | func IsAmazonFIPSEndpoint(endpointURL url.URL) bool { | ||
| 209 | if endpointURL == sentinelURL { | ||
| 210 | return false | ||
| 211 | } | ||
| 212 | return strings.HasPrefix(endpointURL.Host, "s3-fips") && strings.HasSuffix(endpointURL.Host, ".amazonaws.com") | ||
| 213 | } | ||
| 214 | |||
| 215 | // IsAmazonPrivateLinkEndpoint - Match if it is exactly Amazon S3 PrivateLink interface endpoint | ||
| 216 | // See https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html. | ||
| 217 | func IsAmazonPrivateLinkEndpoint(endpointURL url.URL) bool { | ||
| 218 | if endpointURL == sentinelURL { | ||
| 219 | return false | ||
| 220 | } | ||
| 221 | return amazonS3HostPrivateLink.MatchString(endpointURL.Host) | ||
| 222 | } | ||
| 223 | |||
| 224 | // IsGoogleEndpoint - Match if it is exactly Google cloud storage endpoint. | ||
| 225 | func IsGoogleEndpoint(endpointURL url.URL) bool { | ||
| 226 | if endpointURL == sentinelURL { | ||
| 227 | return false | ||
| 228 | } | ||
| 229 | return endpointURL.Host == "storage.googleapis.com" | ||
| 230 | } | ||
| 231 | |||
| 232 | // Expects ascii encoded strings - from output of urlEncodePath | ||
| 233 | func percentEncodeSlash(s string) string { | ||
| 234 | return strings.ReplaceAll(s, "/", "%2F") | ||
| 235 | } | ||
| 236 | |||
| 237 | // QueryEncode - encodes query values in their URL encoded form. In | ||
| 238 | // addition to the percent encoding performed by urlEncodePath() used | ||
| 239 | // here, it also percent encodes '/' (forward slash) | ||
| 240 | func QueryEncode(v url.Values) string { | ||
| 241 | if v == nil { | ||
| 242 | return "" | ||
| 243 | } | ||
| 244 | var buf bytes.Buffer | ||
| 245 | keys := make([]string, 0, len(v)) | ||
| 246 | for k := range v { | ||
| 247 | keys = append(keys, k) | ||
| 248 | } | ||
| 249 | sort.Strings(keys) | ||
| 250 | for _, k := range keys { | ||
| 251 | vs := v[k] | ||
| 252 | prefix := percentEncodeSlash(EncodePath(k)) + "=" | ||
| 253 | for _, v := range vs { | ||
| 254 | if buf.Len() > 0 { | ||
| 255 | buf.WriteByte('&') | ||
| 256 | } | ||
| 257 | buf.WriteString(prefix) | ||
| 258 | buf.WriteString(percentEncodeSlash(EncodePath(v))) | ||
| 259 | } | ||
| 260 | } | ||
| 261 | return buf.String() | ||
| 262 | } | ||
| 263 | |||
| 264 | // TagDecode - decodes canonical tag into map of key and value. | ||
| 265 | func TagDecode(ctag string) map[string]string { | ||
| 266 | if ctag == "" { | ||
| 267 | return map[string]string{} | ||
| 268 | } | ||
| 269 | tags := strings.Split(ctag, "&") | ||
| 270 | tagMap := make(map[string]string, len(tags)) | ||
| 271 | var err error | ||
| 272 | for _, tag := range tags { | ||
| 273 | kvs := strings.SplitN(tag, "=", 2) | ||
| 274 | if len(kvs) == 0 { | ||
| 275 | return map[string]string{} | ||
| 276 | } | ||
| 277 | if len(kvs) == 1 { | ||
| 278 | return map[string]string{} | ||
| 279 | } | ||
| 280 | tagMap[kvs[0]], err = url.PathUnescape(kvs[1]) | ||
| 281 | if err != nil { | ||
| 282 | continue | ||
| 283 | } | ||
| 284 | } | ||
| 285 | return tagMap | ||
| 286 | } | ||
| 287 | |||
| 288 | // TagEncode - encodes tag values in their URL encoded form. In | ||
| 289 | // addition to the percent encoding performed by urlEncodePath() used | ||
| 290 | // here, it also percent encodes '/' (forward slash) | ||
| 291 | func TagEncode(tags map[string]string) string { | ||
| 292 | if tags == nil { | ||
| 293 | return "" | ||
| 294 | } | ||
| 295 | values := url.Values{} | ||
| 296 | for k, v := range tags { | ||
| 297 | values[k] = []string{v} | ||
| 298 | } | ||
| 299 | return QueryEncode(values) | ||
| 300 | } | ||
| 301 | |||
| 302 | // if object matches reserved string, no need to encode them | ||
| 303 | var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") | ||
| 304 | |||
| 305 | // EncodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences | ||
| 306 | // | ||
| 307 | // This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 | ||
| 308 | // non english characters cannot be parsed due to the nature in which url.Encode() is written | ||
| 309 | // | ||
| 310 | // This function on the other hand is a direct replacement for url.Encode() technique to support | ||
| 311 | // pretty much every UTF-8 character. | ||
| 312 | func EncodePath(pathName string) string { | ||
| 313 | if reservedObjectNames.MatchString(pathName) { | ||
| 314 | return pathName | ||
| 315 | } | ||
| 316 | var encodedPathname strings.Builder | ||
| 317 | for _, s := range pathName { | ||
| 318 | if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) | ||
| 319 | encodedPathname.WriteRune(s) | ||
| 320 | continue | ||
| 321 | } | ||
| 322 | switch s { | ||
| 323 | case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) | ||
| 324 | encodedPathname.WriteRune(s) | ||
| 325 | continue | ||
| 326 | default: | ||
| 327 | l := utf8.RuneLen(s) | ||
| 328 | if l < 0 { | ||
| 329 | // if utf8 cannot convert return the same string as is | ||
| 330 | return pathName | ||
| 331 | } | ||
| 332 | u := make([]byte, l) | ||
| 333 | utf8.EncodeRune(u, s) | ||
| 334 | for _, r := range u { | ||
| 335 | hex := hex.EncodeToString([]byte{r}) | ||
| 336 | encodedPathname.WriteString("%" + strings.ToUpper(hex)) | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 340 | return encodedPathname.String() | ||
| 341 | } | ||
| 342 | |||
| 343 | // We support '.' with bucket names but we fallback to using path | ||
| 344 | // style requests instead for such buckets. | ||
| 345 | var ( | ||
| 346 | validBucketName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9\.\-\_\:]{1,61}[A-Za-z0-9]$`) | ||
| 347 | validBucketNameStrict = regexp.MustCompile(`^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$`) | ||
| 348 | ipAddress = regexp.MustCompile(`^(\d+\.){3}\d+$`) | ||
| 349 | ) | ||
| 350 | |||
| 351 | // Common checker for both stricter and basic validation. | ||
| 352 | func checkBucketNameCommon(bucketName string, strict bool) (err error) { | ||
| 353 | if strings.TrimSpace(bucketName) == "" { | ||
| 354 | return errors.New("Bucket name cannot be empty") | ||
| 355 | } | ||
| 356 | if len(bucketName) < 3 { | ||
| 357 | return errors.New("Bucket name cannot be shorter than 3 characters") | ||
| 358 | } | ||
| 359 | if len(bucketName) > 63 { | ||
| 360 | return errors.New("Bucket name cannot be longer than 63 characters") | ||
| 361 | } | ||
| 362 | if ipAddress.MatchString(bucketName) { | ||
| 363 | return errors.New("Bucket name cannot be an ip address") | ||
| 364 | } | ||
| 365 | if strings.Contains(bucketName, "..") || strings.Contains(bucketName, ".-") || strings.Contains(bucketName, "-.") { | ||
| 366 | return errors.New("Bucket name contains invalid characters") | ||
| 367 | } | ||
| 368 | if strict { | ||
| 369 | if !validBucketNameStrict.MatchString(bucketName) { | ||
| 370 | err = errors.New("Bucket name contains invalid characters") | ||
| 371 | } | ||
| 372 | return err | ||
| 373 | } | ||
| 374 | if !validBucketName.MatchString(bucketName) { | ||
| 375 | err = errors.New("Bucket name contains invalid characters") | ||
| 376 | } | ||
| 377 | return err | ||
| 378 | } | ||
| 379 | |||
| 380 | // CheckValidBucketName - checks if we have a valid input bucket name. | ||
| 381 | func CheckValidBucketName(bucketName string) (err error) { | ||
| 382 | return checkBucketNameCommon(bucketName, false) | ||
| 383 | } | ||
| 384 | |||
| 385 | // CheckValidBucketNameStrict - checks if we have a valid input bucket name. | ||
| 386 | // This is a stricter version. | ||
| 387 | // - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html | ||
| 388 | func CheckValidBucketNameStrict(bucketName string) (err error) { | ||
| 389 | return checkBucketNameCommon(bucketName, true) | ||
| 390 | } | ||
| 391 | |||
| 392 | // CheckValidObjectNamePrefix - checks if we have a valid input object name prefix. | ||
| 393 | // - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html | ||
| 394 | func CheckValidObjectNamePrefix(objectName string) error { | ||
| 395 | if len(objectName) > 1024 { | ||
| 396 | return errors.New("Object name cannot be longer than 1024 characters") | ||
| 397 | } | ||
| 398 | if !utf8.ValidString(objectName) { | ||
| 399 | return errors.New("Object name with non UTF-8 strings are not supported") | ||
| 400 | } | ||
| 401 | return nil | ||
| 402 | } | ||
| 403 | |||
| 404 | // CheckValidObjectName - checks if we have a valid input object name. | ||
| 405 | // - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html | ||
| 406 | func CheckValidObjectName(objectName string) error { | ||
| 407 | if strings.TrimSpace(objectName) == "" { | ||
| 408 | return errors.New("Object name cannot be empty") | ||
| 409 | } | ||
| 410 | return CheckValidObjectNamePrefix(objectName) | ||
| 411 | } | ||