utils.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  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. package minio
  18. import (
  19. "context"
  20. "crypto/md5"
  21. "encoding/base64"
  22. "encoding/hex"
  23. "encoding/xml"
  24. "errors"
  25. "fmt"
  26. "hash"
  27. "io"
  28. "io/ioutil"
  29. "math/rand"
  30. "net"
  31. "net/http"
  32. "net/url"
  33. "regexp"
  34. "strconv"
  35. "strings"
  36. "sync"
  37. "time"
  38. md5simd "github.com/minio/md5-simd"
  39. "github.com/minio/minio-go/v7/pkg/s3utils"
  40. "github.com/minio/sha256-simd"
  41. )
  42. func trimEtag(etag string) string {
  43. etag = strings.TrimPrefix(etag, "\"")
  44. return strings.TrimSuffix(etag, "\"")
  45. }
  46. var expirationRegex = regexp.MustCompile(`expiry-date="(.*?)", rule-id="(.*?)"`)
  47. func amzExpirationToExpiryDateRuleID(expiration string) (time.Time, string) {
  48. if matches := expirationRegex.FindStringSubmatch(expiration); len(matches) == 3 {
  49. expTime, err := time.Parse(http.TimeFormat, matches[1])
  50. if err != nil {
  51. return time.Time{}, ""
  52. }
  53. return expTime, matches[2]
  54. }
  55. return time.Time{}, ""
  56. }
  57. var restoreRegex = regexp.MustCompile(`ongoing-request="(.*?)"(, expiry-date="(.*?)")?`)
  58. func amzRestoreToStruct(restore string) (ongoing bool, expTime time.Time, err error) {
  59. matches := restoreRegex.FindStringSubmatch(restore)
  60. if len(matches) != 4 {
  61. return false, time.Time{}, errors.New("unexpected restore header")
  62. }
  63. ongoing, err = strconv.ParseBool(matches[1])
  64. if err != nil {
  65. return false, time.Time{}, err
  66. }
  67. if matches[3] != "" {
  68. expTime, err = time.Parse(http.TimeFormat, matches[3])
  69. if err != nil {
  70. return false, time.Time{}, err
  71. }
  72. }
  73. return
  74. }
  75. // xmlDecoder provide decoded value in xml.
  76. func xmlDecoder(body io.Reader, v interface{}) error {
  77. d := xml.NewDecoder(body)
  78. return d.Decode(v)
  79. }
  80. // sum256 calculate sha256sum for an input byte array, returns hex encoded.
  81. func sum256Hex(data []byte) string {
  82. hash := newSHA256Hasher()
  83. defer hash.Close()
  84. hash.Write(data)
  85. return hex.EncodeToString(hash.Sum(nil))
  86. }
  87. // sumMD5Base64 calculate md5sum for an input byte array, returns base64 encoded.
  88. func sumMD5Base64(data []byte) string {
  89. hash := newMd5Hasher()
  90. defer hash.Close()
  91. hash.Write(data)
  92. return base64.StdEncoding.EncodeToString(hash.Sum(nil))
  93. }
  94. // getEndpointURL - construct a new endpoint.
  95. func getEndpointURL(endpoint string, secure bool) (*url.URL, error) {
  96. if strings.Contains(endpoint, ":") {
  97. host, _, err := net.SplitHostPort(endpoint)
  98. if err != nil {
  99. return nil, err
  100. }
  101. if !s3utils.IsValidIP(host) && !s3utils.IsValidDomain(host) {
  102. msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards."
  103. return nil, errInvalidArgument(msg)
  104. }
  105. } else {
  106. if !s3utils.IsValidIP(endpoint) && !s3utils.IsValidDomain(endpoint) {
  107. msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards."
  108. return nil, errInvalidArgument(msg)
  109. }
  110. }
  111. // If secure is false, use 'http' scheme.
  112. scheme := "https"
  113. if !secure {
  114. scheme = "http"
  115. }
  116. // Construct a secured endpoint URL.
  117. endpointURLStr := scheme + "://" + endpoint
  118. endpointURL, err := url.Parse(endpointURLStr)
  119. if err != nil {
  120. return nil, err
  121. }
  122. // Validate incoming endpoint URL.
  123. if err := isValidEndpointURL(*endpointURL); err != nil {
  124. return nil, err
  125. }
  126. return endpointURL, nil
  127. }
  128. // closeResponse close non nil response with any response Body.
  129. // convenient wrapper to drain any remaining data on response body.
  130. //
  131. // Subsequently this allows golang http RoundTripper
  132. // to re-use the same connection for future requests.
  133. func closeResponse(resp *http.Response) {
  134. // Callers should close resp.Body when done reading from it.
  135. // If resp.Body is not closed, the Client's underlying RoundTripper
  136. // (typically Transport) may not be able to re-use a persistent TCP
  137. // connection to the server for a subsequent "keep-alive" request.
  138. if resp != nil && resp.Body != nil {
  139. // Drain any remaining Body and then close the connection.
  140. // Without this closing connection would disallow re-using
  141. // the same connection for future uses.
  142. // - http://stackoverflow.com/a/17961593/4465767
  143. io.Copy(ioutil.Discard, resp.Body)
  144. resp.Body.Close()
  145. }
  146. }
  147. var (
  148. // Hex encoded string of nil sha256sum bytes.
  149. emptySHA256Hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
  150. // Sentinel URL is the default url value which is invalid.
  151. sentinelURL = url.URL{}
  152. )
  153. // Verify if input endpoint URL is valid.
  154. func isValidEndpointURL(endpointURL url.URL) error {
  155. if endpointURL == sentinelURL {
  156. return errInvalidArgument("Endpoint url cannot be empty.")
  157. }
  158. if endpointURL.Path != "/" && endpointURL.Path != "" {
  159. return errInvalidArgument("Endpoint url cannot have fully qualified paths.")
  160. }
  161. if strings.Contains(endpointURL.Host, ".s3.amazonaws.com") {
  162. if !s3utils.IsAmazonEndpoint(endpointURL) {
  163. return errInvalidArgument("Amazon S3 endpoint should be 's3.amazonaws.com'.")
  164. }
  165. }
  166. if strings.Contains(endpointURL.Host, ".googleapis.com") {
  167. if !s3utils.IsGoogleEndpoint(endpointURL) {
  168. return errInvalidArgument("Google Cloud Storage endpoint should be 'storage.googleapis.com'.")
  169. }
  170. }
  171. return nil
  172. }
  173. // Verify if input expires value is valid.
  174. func isValidExpiry(expires time.Duration) error {
  175. expireSeconds := int64(expires / time.Second)
  176. if expireSeconds < 1 {
  177. return errInvalidArgument("Expires cannot be lesser than 1 second.")
  178. }
  179. if expireSeconds > 604800 {
  180. return errInvalidArgument("Expires cannot be greater than 7 days.")
  181. }
  182. return nil
  183. }
  184. // Extract only necessary metadata header key/values by
  185. // filtering them out with a list of custom header keys.
  186. func extractObjMetadata(header http.Header) http.Header {
  187. preserveKeys := []string{
  188. "Content-Type",
  189. "Cache-Control",
  190. "Content-Encoding",
  191. "Content-Language",
  192. "Content-Disposition",
  193. "X-Amz-Storage-Class",
  194. "X-Amz-Object-Lock-Mode",
  195. "X-Amz-Object-Lock-Retain-Until-Date",
  196. "X-Amz-Object-Lock-Legal-Hold",
  197. "X-Amz-Website-Redirect-Location",
  198. "X-Amz-Server-Side-Encryption",
  199. "X-Amz-Tagging-Count",
  200. "X-Amz-Meta-",
  201. // Add new headers to be preserved.
  202. // if you add new headers here, please extend
  203. // PutObjectOptions{} to preserve them
  204. // upon upload as well.
  205. }
  206. filteredHeader := make(http.Header)
  207. for k, v := range header {
  208. var found bool
  209. for _, prefix := range preserveKeys {
  210. if !strings.HasPrefix(k, prefix) {
  211. continue
  212. }
  213. found = true
  214. break
  215. }
  216. if found {
  217. filteredHeader[k] = v
  218. }
  219. }
  220. return filteredHeader
  221. }
  222. // ToObjectInfo converts http header values into ObjectInfo type,
  223. // extracts metadata and fills in all the necessary fields in ObjectInfo.
  224. func ToObjectInfo(bucketName string, objectName string, h http.Header) (ObjectInfo, error) {
  225. var err error
  226. // Trim off the odd double quotes from ETag in the beginning and end.
  227. etag := trimEtag(h.Get("ETag"))
  228. // Parse content length is exists
  229. var size int64 = -1
  230. contentLengthStr := h.Get("Content-Length")
  231. if contentLengthStr != "" {
  232. size, err = strconv.ParseInt(contentLengthStr, 10, 64)
  233. if err != nil {
  234. // Content-Length is not valid
  235. return ObjectInfo{}, ErrorResponse{
  236. Code: "InternalError",
  237. Message: fmt.Sprintf("Content-Length is not an integer, failed with %v", err),
  238. BucketName: bucketName,
  239. Key: objectName,
  240. RequestID: h.Get("x-amz-request-id"),
  241. HostID: h.Get("x-amz-id-2"),
  242. Region: h.Get("x-amz-bucket-region"),
  243. }
  244. }
  245. }
  246. // Parse Last-Modified has http time format.
  247. date, err := time.Parse(http.TimeFormat, h.Get("Last-Modified"))
  248. if err != nil {
  249. return ObjectInfo{}, ErrorResponse{
  250. Code: "InternalError",
  251. Message: fmt.Sprintf("Last-Modified time format is invalid, failed with %v", err),
  252. BucketName: bucketName,
  253. Key: objectName,
  254. RequestID: h.Get("x-amz-request-id"),
  255. HostID: h.Get("x-amz-id-2"),
  256. Region: h.Get("x-amz-bucket-region"),
  257. }
  258. }
  259. // Fetch content type if any present.
  260. contentType := strings.TrimSpace(h.Get("Content-Type"))
  261. if contentType == "" {
  262. contentType = "application/octet-stream"
  263. }
  264. expiryStr := h.Get("Expires")
  265. var expiry time.Time
  266. if expiryStr != "" {
  267. expiry, _ = time.Parse(http.TimeFormat, expiryStr)
  268. }
  269. metadata := extractObjMetadata(h)
  270. userMetadata := make(map[string]string)
  271. for k, v := range metadata {
  272. if strings.HasPrefix(k, "X-Amz-Meta-") {
  273. userMetadata[strings.TrimPrefix(k, "X-Amz-Meta-")] = v[0]
  274. }
  275. }
  276. userTags := s3utils.TagDecode(h.Get(amzTaggingHeader))
  277. var tagCount int
  278. if count := h.Get(amzTaggingCount); count != "" {
  279. tagCount, err = strconv.Atoi(count)
  280. if err != nil {
  281. return ObjectInfo{}, ErrorResponse{
  282. Code: "InternalError",
  283. Message: fmt.Sprintf("x-amz-tagging-count is not an integer, failed with %v", err),
  284. BucketName: bucketName,
  285. Key: objectName,
  286. RequestID: h.Get("x-amz-request-id"),
  287. HostID: h.Get("x-amz-id-2"),
  288. Region: h.Get("x-amz-bucket-region"),
  289. }
  290. }
  291. }
  292. // Nil if not found
  293. var restore *RestoreInfo
  294. if restoreHdr := h.Get(amzRestore); restoreHdr != "" {
  295. ongoing, expTime, err := amzRestoreToStruct(restoreHdr)
  296. if err != nil {
  297. return ObjectInfo{}, err
  298. }
  299. restore = &RestoreInfo{OngoingRestore: ongoing, ExpiryTime: expTime}
  300. }
  301. // extract lifecycle expiry date and rule ID
  302. expTime, ruleID := amzExpirationToExpiryDateRuleID(h.Get(amzExpiration))
  303. deleteMarker := h.Get(amzDeleteMarker) == "true"
  304. // Save object metadata info.
  305. return ObjectInfo{
  306. ETag: etag,
  307. Key: objectName,
  308. Size: size,
  309. LastModified: date,
  310. ContentType: contentType,
  311. Expires: expiry,
  312. VersionID: h.Get(amzVersionID),
  313. IsDeleteMarker: deleteMarker,
  314. ReplicationStatus: h.Get(amzReplicationStatus),
  315. Expiration: expTime,
  316. ExpirationRuleID: ruleID,
  317. // Extract only the relevant header keys describing the object.
  318. // following function filters out a list of standard set of keys
  319. // which are not part of object metadata.
  320. Metadata: metadata,
  321. UserMetadata: userMetadata,
  322. UserTags: userTags,
  323. UserTagCount: tagCount,
  324. Restore: restore,
  325. }, nil
  326. }
  327. var readFull = func(r io.Reader, buf []byte) (n int, err error) {
  328. // ReadFull reads exactly len(buf) bytes from r into buf.
  329. // It returns the number of bytes copied and an error if
  330. // fewer bytes were read. The error is EOF only if no bytes
  331. // were read. If an EOF happens after reading some but not
  332. // all the bytes, ReadFull returns ErrUnexpectedEOF.
  333. // On return, n == len(buf) if and only if err == nil.
  334. // If r returns an error having read at least len(buf) bytes,
  335. // the error is dropped.
  336. for n < len(buf) && err == nil {
  337. var nn int
  338. nn, err = r.Read(buf[n:])
  339. // Some spurious io.Reader's return
  340. // io.ErrUnexpectedEOF when nn == 0
  341. // this behavior is undocumented
  342. // so we are on purpose not using io.ReadFull
  343. // implementation because this can lead
  344. // to custom handling, to avoid that
  345. // we simply modify the original io.ReadFull
  346. // implementation to avoid this issue.
  347. // io.ErrUnexpectedEOF with nn == 0 really
  348. // means that io.EOF
  349. if err == io.ErrUnexpectedEOF && nn == 0 {
  350. err = io.EOF
  351. }
  352. n += nn
  353. }
  354. if n >= len(buf) {
  355. err = nil
  356. } else if n > 0 && err == io.EOF {
  357. err = io.ErrUnexpectedEOF
  358. }
  359. return
  360. }
  361. // regCred matches credential string in HTTP header
  362. var regCred = regexp.MustCompile("Credential=([A-Z0-9]+)/")
  363. // regCred matches signature string in HTTP header
  364. var regSign = regexp.MustCompile("Signature=([[0-9a-f]+)")
  365. // Redact out signature value from authorization string.
  366. func redactSignature(origAuth string) string {
  367. if !strings.HasPrefix(origAuth, signV4Algorithm) {
  368. // Set a temporary redacted auth
  369. return "AWS **REDACTED**:**REDACTED**"
  370. }
  371. /// Signature V4 authorization header.
  372. // Strip out accessKeyID from:
  373. // Credential=<access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request
  374. newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/")
  375. // Strip out 256-bit signature from: Signature=<256-bit signature>
  376. return regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**")
  377. }
  378. // Get default location returns the location based on the input
  379. // URL `u`, if region override is provided then all location
  380. // defaults to regionOverride.
  381. //
  382. // If no other cases match then the location is set to `us-east-1`
  383. // as a last resort.
  384. func getDefaultLocation(u url.URL, regionOverride string) (location string) {
  385. if regionOverride != "" {
  386. return regionOverride
  387. }
  388. region := s3utils.GetRegionFromURL(u)
  389. if region == "" {
  390. region = "us-east-1"
  391. }
  392. return region
  393. }
  394. var supportedHeaders = map[string]bool{
  395. "content-type": true,
  396. "cache-control": true,
  397. "content-encoding": true,
  398. "content-disposition": true,
  399. "content-language": true,
  400. "x-amz-website-redirect-location": true,
  401. "x-amz-object-lock-mode": true,
  402. "x-amz-metadata-directive": true,
  403. "x-amz-object-lock-retain-until-date": true,
  404. "expires": true,
  405. "x-amz-replication-status": true,
  406. // Add more supported headers here.
  407. // Must be lower case.
  408. }
  409. // isStorageClassHeader returns true if the header is a supported storage class header
  410. func isStorageClassHeader(headerKey string) bool {
  411. return strings.EqualFold(amzStorageClass, headerKey)
  412. }
  413. // isStandardHeader returns true if header is a supported header and not a custom header
  414. func isStandardHeader(headerKey string) bool {
  415. return supportedHeaders[strings.ToLower(headerKey)]
  416. }
  417. // sseHeaders is list of server side encryption headers
  418. var sseHeaders = map[string]bool{
  419. "x-amz-server-side-encryption": true,
  420. "x-amz-server-side-encryption-aws-kms-key-id": true,
  421. "x-amz-server-side-encryption-context": true,
  422. "x-amz-server-side-encryption-customer-algorithm": true,
  423. "x-amz-server-side-encryption-customer-key": true,
  424. "x-amz-server-side-encryption-customer-key-md5": true,
  425. // Add more supported headers here.
  426. // Must be lower case.
  427. }
  428. // isSSEHeader returns true if header is a server side encryption header.
  429. func isSSEHeader(headerKey string) bool {
  430. return sseHeaders[strings.ToLower(headerKey)]
  431. }
  432. // isAmzHeader returns true if header is a x-amz-meta-* or x-amz-acl header.
  433. func isAmzHeader(headerKey string) bool {
  434. key := strings.ToLower(headerKey)
  435. return strings.HasPrefix(key, "x-amz-meta-") || strings.HasPrefix(key, "x-amz-grant-") || key == "x-amz-acl" || isSSEHeader(headerKey)
  436. }
  437. var md5Pool = sync.Pool{New: func() interface{} { return md5.New() }}
  438. var sha256Pool = sync.Pool{New: func() interface{} { return sha256.New() }}
  439. func newMd5Hasher() md5simd.Hasher {
  440. return hashWrapper{Hash: md5Pool.New().(hash.Hash), isMD5: true}
  441. }
  442. func newSHA256Hasher() md5simd.Hasher {
  443. return hashWrapper{Hash: sha256Pool.New().(hash.Hash), isSHA256: true}
  444. }
  445. // hashWrapper implements the md5simd.Hasher interface.
  446. type hashWrapper struct {
  447. hash.Hash
  448. isMD5 bool
  449. isSHA256 bool
  450. }
  451. // Close will put the hasher back into the pool.
  452. func (m hashWrapper) Close() {
  453. if m.isMD5 && m.Hash != nil {
  454. m.Reset()
  455. md5Pool.Put(m.Hash)
  456. }
  457. if m.isSHA256 && m.Hash != nil {
  458. m.Reset()
  459. sha256Pool.Put(m.Hash)
  460. }
  461. m.Hash = nil
  462. }
  463. const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569"
  464. const (
  465. letterIdxBits = 6 // 6 bits to represent a letter index
  466. letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
  467. letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
  468. )
  469. // randString generates random names and prepends them with a known prefix.
  470. func randString(n int, src rand.Source, prefix string) string {
  471. b := make([]byte, n)
  472. // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
  473. for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
  474. if remain == 0 {
  475. cache, remain = src.Int63(), letterIdxMax
  476. }
  477. if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
  478. b[i] = letterBytes[idx]
  479. i--
  480. }
  481. cache >>= letterIdxBits
  482. remain--
  483. }
  484. return prefix + string(b[0:30-len(prefix)])
  485. }
  486. // IsNetworkOrHostDown - if there was a network error or if the host is down.
  487. // expectTimeouts indicates that *context* timeouts are expected and does not
  488. // indicate a downed host. Other timeouts still returns down.
  489. func IsNetworkOrHostDown(err error, expectTimeouts bool) bool {
  490. if err == nil {
  491. return false
  492. }
  493. if errors.Is(err, context.Canceled) {
  494. return false
  495. }
  496. if expectTimeouts && errors.Is(err, context.DeadlineExceeded) {
  497. return false
  498. }
  499. // We need to figure if the error either a timeout
  500. // or a non-temporary error.
  501. urlErr := &url.Error{}
  502. if errors.As(err, &urlErr) {
  503. switch urlErr.Err.(type) {
  504. case *net.DNSError, *net.OpError, net.UnknownNetworkError:
  505. return true
  506. }
  507. }
  508. var e net.Error
  509. if errors.As(err, &e) {
  510. if e.Timeout() {
  511. return true
  512. }
  513. }
  514. // Fallback to other mechanisms.
  515. switch {
  516. case strings.Contains(err.Error(), "Connection closed by foreign host"):
  517. return true
  518. case strings.Contains(err.Error(), "TLS handshake timeout"):
  519. // If error is - tlsHandshakeTimeoutError.
  520. return true
  521. case strings.Contains(err.Error(), "i/o timeout"):
  522. // If error is - tcp timeoutError.
  523. return true
  524. case strings.Contains(err.Error(), "connection timed out"):
  525. // If err is a net.Dial timeout.
  526. return true
  527. case strings.Contains(strings.ToLower(err.Error()), "503 service unavailable"):
  528. // Denial errors
  529. return true
  530. }
  531. return false
  532. }