You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

523 lines
16 KiB

  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. package cmd
  18. import (
  19. "bytes"
  20. "context"
  21. "errors"
  22. "io"
  23. "mime"
  24. "net/http"
  25. "path/filepath"
  26. "sort"
  27. "strings"
  28. "github.com/minio/minio/internal/auth"
  29. "github.com/minio/minio/internal/crypto"
  30. xhttp "github.com/minio/minio/internal/http"
  31. xioutil "github.com/minio/minio/internal/ioutil"
  32. "github.com/minio/pkg/v3/policy"
  33. "github.com/minio/zipindex"
  34. )
  35. const (
  36. archiveType = "zip"
  37. archiveTypeEnc = "zip-enc"
  38. archiveExt = "." + archiveType // ".zip"
  39. archiveSeparator = "/"
  40. archivePattern = archiveExt + archiveSeparator // ".zip/"
  41. archiveTypeMetadataKey = ReservedMetadataPrefixLower + "archive-type" // "x-minio-internal-archive-type"
  42. archiveInfoMetadataKey = ReservedMetadataPrefixLower + "archive-info" // "x-minio-internal-archive-info"
  43. // Peek into a zip archive
  44. xMinIOExtract = "x-minio-extract"
  45. )
  46. // splitZipExtensionPath splits the S3 path to the zip file and the path inside the zip:
  47. //
  48. // e.g /path/to/archive.zip/backup-2021/myimage.png => /path/to/archive.zip, backup/myimage.png
  49. func splitZipExtensionPath(input string) (zipPath, object string, err error) {
  50. idx := strings.Index(input, archivePattern)
  51. if idx < 0 {
  52. // Should never happen
  53. return "", "", errors.New("unable to parse zip path")
  54. }
  55. return input[:idx+len(archivePattern)-1], input[idx+len(archivePattern):], nil
  56. }
  57. // getObjectInArchiveFileHandler - GET Object in the archive file
  58. func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context, objectAPI ObjectLayer, bucket, object string, w http.ResponseWriter, r *http.Request) {
  59. if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
  60. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
  61. return
  62. }
  63. zipPath, object, err := splitZipExtensionPath(object)
  64. if err != nil {
  65. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  66. return
  67. }
  68. opts, err := getOpts(ctx, r, bucket, zipPath)
  69. if err != nil {
  70. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  71. return
  72. }
  73. getObjectInfo := objectAPI.GetObjectInfo
  74. // Check for auth type to return S3 compatible error.
  75. // type to return the correct error (NoSuchKey vs AccessDenied)
  76. if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, zipPath); s3Error != ErrNone {
  77. if getRequestAuthType(r) == authTypeAnonymous {
  78. // As per "Permission" section in
  79. // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
  80. // If the object you request does not exist,
  81. // the error Amazon S3 returns depends on
  82. // whether you also have the s3:ListBucket
  83. // permission.
  84. // * If you have the s3:ListBucket permission
  85. // on the bucket, Amazon S3 will return an
  86. // HTTP status code 404 ("no such key")
  87. // error.
  88. // * if you don’t have the s3:ListBucket
  89. // permission, Amazon S3 will return an HTTP
  90. // status code 403 ("access denied") error.`
  91. if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
  92. Action: policy.ListBucketAction,
  93. BucketName: bucket,
  94. ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
  95. IsOwner: false,
  96. }) {
  97. _, err = getObjectInfo(ctx, bucket, zipPath, opts)
  98. if toAPIError(ctx, err).Code == "NoSuchKey" {
  99. s3Error = ErrNoSuchKey
  100. }
  101. }
  102. }
  103. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
  104. return
  105. }
  106. // We do not allow offsetting into extracted files.
  107. if opts.PartNumber != 0 {
  108. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartNumber), r.URL)
  109. return
  110. }
  111. if r.Header.Get(xhttp.Range) != "" {
  112. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRange), r.URL)
  113. return
  114. }
  115. // Validate pre-conditions if any.
  116. opts.CheckPrecondFn = func(oi ObjectInfo) bool {
  117. if _, err := DecryptObjectInfo(&oi, r); err != nil {
  118. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  119. return true
  120. }
  121. return checkPreconditions(ctx, w, r, oi, opts)
  122. }
  123. zipObjInfo, err := getObjectInfo(ctx, bucket, zipPath, opts)
  124. if err != nil {
  125. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  126. return
  127. }
  128. zipInfo := zipObjInfo.ArchiveInfo(r.Header)
  129. if len(zipInfo) == 0 {
  130. opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
  131. if err != nil {
  132. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  133. return
  134. }
  135. zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, opts)
  136. }
  137. if err != nil {
  138. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  139. return
  140. }
  141. file, err := zipindex.FindSerialized(zipInfo, object)
  142. if err != nil {
  143. if err == io.EOF {
  144. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchKey), r.URL)
  145. } else {
  146. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  147. }
  148. return
  149. }
  150. // New object info
  151. fileObjInfo := ObjectInfo{
  152. Bucket: bucket,
  153. Name: object,
  154. Size: int64(file.UncompressedSize64),
  155. ModTime: zipObjInfo.ModTime,
  156. ContentType: mime.TypeByExtension(filepath.Ext(object)),
  157. }
  158. var rc io.ReadCloser
  159. if file.UncompressedSize64 > 0 {
  160. // There may be number of header bytes before the content.
  161. // Reading 64K extra. This should more than cover name and any "extra" details.
  162. end := file.Offset + int64(file.CompressedSize64) + 64<<10
  163. if end > zipObjInfo.Size {
  164. end = zipObjInfo.Size
  165. }
  166. rs := &HTTPRangeSpec{Start: file.Offset, End: end}
  167. gr, err := objectAPI.GetObjectNInfo(ctx, bucket, zipPath, rs, nil, opts)
  168. if err != nil {
  169. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  170. return
  171. }
  172. defer gr.Close()
  173. rc, err = file.Open(gr)
  174. if err != nil {
  175. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  176. return
  177. }
  178. } else {
  179. rc = io.NopCloser(bytes.NewReader([]byte{}))
  180. }
  181. defer rc.Close()
  182. if err = setObjectHeaders(ctx, w, fileObjInfo, nil, opts); err != nil {
  183. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  184. return
  185. }
  186. // s3zip does not allow ranges
  187. w.Header().Del(xhttp.AcceptRanges)
  188. setHeadGetRespHeaders(w, r.Form)
  189. httpWriter := xioutil.WriteOnClose(w)
  190. // Write object content to response body
  191. if _, err = xioutil.Copy(httpWriter, rc); err != nil {
  192. if !httpWriter.HasWritten() {
  193. // write error response only if no data or headers has been written to client yet
  194. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  195. return
  196. }
  197. return
  198. }
  199. if err = httpWriter.Close(); err != nil {
  200. if !httpWriter.HasWritten() { // write error response only if no data or headers has been written to client yet
  201. writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  202. return
  203. }
  204. return
  205. }
  206. }
  207. // listObjectsV2InArchive generates S3 listing result ListObjectsV2Info from zip file, all parameters are already validated by the caller.
  208. func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket, prefix, token, delimiter string, maxKeys int, startAfter string, h http.Header) (ListObjectsV2Info, error) {
  209. zipPath, _, err := splitZipExtensionPath(prefix)
  210. if err != nil {
  211. // Return empty listing
  212. return ListObjectsV2Info{}, nil
  213. }
  214. zipObjInfo, err := objectAPI.GetObjectInfo(ctx, bucket, zipPath, ObjectOptions{})
  215. if err != nil {
  216. // Return empty listing
  217. return ListObjectsV2Info{}, nil
  218. }
  219. zipInfo := zipObjInfo.ArchiveInfo(h)
  220. if len(zipInfo) == 0 {
  221. // Always update the latest version
  222. zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, ObjectOptions{})
  223. }
  224. if err != nil {
  225. return ListObjectsV2Info{}, err
  226. }
  227. files, err := zipindex.DeserializeFiles(zipInfo)
  228. if err != nil {
  229. return ListObjectsV2Info{}, err
  230. }
  231. sort.Slice(files, func(i, j int) bool {
  232. return files[i].Name < files[j].Name
  233. })
  234. var (
  235. count int
  236. isTruncated bool
  237. nextToken string
  238. listObjectsInfo ListObjectsV2Info
  239. )
  240. // Always set this
  241. listObjectsInfo.ContinuationToken = token
  242. // Open and iterate through the files in the archive.
  243. for _, file := range files {
  244. objName := zipObjInfo.Name + archiveSeparator + file.Name
  245. if objName <= startAfter || objName <= token {
  246. continue
  247. }
  248. if strings.HasPrefix(objName, prefix) {
  249. if count == maxKeys {
  250. isTruncated = true
  251. break
  252. }
  253. if delimiter != "" {
  254. i := strings.Index(objName[len(prefix):], delimiter)
  255. if i >= 0 {
  256. commonPrefix := objName[:len(prefix)+i+1]
  257. if len(listObjectsInfo.Prefixes) == 0 || commonPrefix != listObjectsInfo.Prefixes[len(listObjectsInfo.Prefixes)-1] {
  258. listObjectsInfo.Prefixes = append(listObjectsInfo.Prefixes, commonPrefix)
  259. count++
  260. }
  261. goto next
  262. }
  263. }
  264. listObjectsInfo.Objects = append(listObjectsInfo.Objects, ObjectInfo{
  265. Bucket: bucket,
  266. Name: objName,
  267. Size: int64(file.UncompressedSize64),
  268. ModTime: zipObjInfo.ModTime,
  269. })
  270. count++
  271. }
  272. next:
  273. nextToken = objName
  274. }
  275. if isTruncated {
  276. listObjectsInfo.IsTruncated = true
  277. listObjectsInfo.NextContinuationToken = nextToken
  278. }
  279. return listObjectsInfo, nil
  280. }
  281. // getFilesFromZIPObject reads a partial stream of a zip file to build the zipindex.Files index
  282. func getFilesListFromZIPObject(ctx context.Context, objectAPI ObjectLayer, bucket, object string, opts ObjectOptions) (zipindex.Files, ObjectInfo, error) {
  283. size := 1 << 20
  284. var objSize int64
  285. for {
  286. rs := &HTTPRangeSpec{IsSuffixLength: true, Start: int64(-size)}
  287. gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, rs, nil, opts)
  288. if err != nil {
  289. return nil, ObjectInfo{}, err
  290. }
  291. b, err := io.ReadAll(gr)
  292. gr.Close()
  293. if err != nil {
  294. return nil, ObjectInfo{}, err
  295. }
  296. if size > len(b) {
  297. size = len(b)
  298. }
  299. // Calculate the object real size if encrypted
  300. if _, ok := crypto.IsEncrypted(gr.ObjInfo.UserDefined); ok {
  301. objSize, err = gr.ObjInfo.DecryptedSize()
  302. if err != nil {
  303. return nil, ObjectInfo{}, err
  304. }
  305. } else {
  306. objSize = gr.ObjInfo.Size
  307. }
  308. files, err := zipindex.ReadDir(b[len(b)-size:], objSize, nil)
  309. if err == nil {
  310. return files, gr.ObjInfo, nil
  311. }
  312. var terr zipindex.ErrNeedMoreData
  313. if errors.As(err, &terr) {
  314. size = int(terr.FromEnd)
  315. if size <= 0 || size > 100<<20 {
  316. return nil, ObjectInfo{}, errors.New("zip directory too large")
  317. }
  318. } else {
  319. return nil, ObjectInfo{}, err
  320. }
  321. }
  322. }
  323. // headObjectInArchiveFileHandler - HEAD Object in an archive file
  324. func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context, objectAPI ObjectLayer, bucket, object string, w http.ResponseWriter, r *http.Request) {
  325. if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
  326. writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrBadRequest))
  327. return
  328. }
  329. zipPath, object, err := splitZipExtensionPath(object)
  330. if err != nil {
  331. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  332. return
  333. }
  334. getObjectInfo := objectAPI.GetObjectInfo
  335. opts, err := getOpts(ctx, r, bucket, zipPath)
  336. if err != nil {
  337. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  338. return
  339. }
  340. if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, zipPath); s3Error != ErrNone {
  341. if getRequestAuthType(r) == authTypeAnonymous {
  342. // As per "Permission" section in
  343. // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
  344. // If the object you request does not exist,
  345. // the error Amazon S3 returns depends on
  346. // whether you also have the s3:ListBucket
  347. // permission.
  348. // * If you have the s3:ListBucket permission
  349. // on the bucket, Amazon S3 will return an
  350. // HTTP status code 404 ("no such key")
  351. // error.
  352. // * if you don’t have the s3:ListBucket
  353. // permission, Amazon S3 will return an HTTP
  354. // status code 403 ("access denied") error.`
  355. if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
  356. Action: policy.ListBucketAction,
  357. BucketName: bucket,
  358. ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
  359. IsOwner: false,
  360. }) {
  361. _, err = getObjectInfo(ctx, bucket, zipPath, opts)
  362. if toAPIError(ctx, err).Code == "NoSuchKey" {
  363. s3Error = ErrNoSuchKey
  364. }
  365. }
  366. }
  367. errCode := errorCodes.ToAPIErr(s3Error)
  368. w.Header().Set(xMinIOErrCodeHeader, errCode.Code)
  369. w.Header().Set(xMinIOErrDescHeader, "\""+errCode.Description+"\"")
  370. writeErrorResponseHeadersOnly(w, errCode)
  371. return
  372. }
  373. // Validate pre-conditions if any.
  374. opts.CheckPrecondFn = func(oi ObjectInfo) bool {
  375. return checkPreconditions(ctx, w, r, oi, opts)
  376. }
  377. // We do not allow offsetting into extracted files.
  378. if opts.PartNumber != 0 {
  379. writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInvalidPartNumber))
  380. return
  381. }
  382. if r.Header.Get(xhttp.Range) != "" {
  383. writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInvalidRange))
  384. return
  385. }
  386. zipObjInfo, err := getObjectInfo(ctx, bucket, zipPath, opts)
  387. if err != nil {
  388. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  389. return
  390. }
  391. zipInfo := zipObjInfo.ArchiveInfo(r.Header)
  392. if len(zipInfo) == 0 {
  393. opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
  394. if err != nil {
  395. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  396. return
  397. }
  398. zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, opts)
  399. }
  400. if err != nil {
  401. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  402. return
  403. }
  404. file, err := zipindex.FindSerialized(zipInfo, object)
  405. if err != nil {
  406. if err == io.EOF {
  407. writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
  408. } else {
  409. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  410. }
  411. return
  412. }
  413. objInfo := ObjectInfo{
  414. Bucket: bucket,
  415. Name: file.Name,
  416. Size: int64(file.UncompressedSize64),
  417. ModTime: zipObjInfo.ModTime,
  418. }
  419. // Set standard object headers.
  420. if err = setObjectHeaders(ctx, w, objInfo, nil, opts); err != nil {
  421. writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
  422. return
  423. }
  424. // s3zip does not allow ranges.
  425. w.Header().Del(xhttp.AcceptRanges)
  426. // Set any additional requested response headers.
  427. setHeadGetRespHeaders(w, r.Form)
  428. // Successful response.
  429. w.WriteHeader(http.StatusOK)
  430. }
  431. // Update the passed zip object metadata with the zip contents info, file name, modtime, size, etc.
  432. // The returned zip index will de decrypted.
  433. func updateObjectMetadataWithZipInfo(ctx context.Context, objectAPI ObjectLayer, bucket, object string, opts ObjectOptions) ([]byte, error) {
  434. files, srcInfo, err := getFilesListFromZIPObject(ctx, objectAPI, bucket, object, opts)
  435. if err != nil {
  436. return nil, err
  437. }
  438. files.OptimizeSize()
  439. zipInfo, err := files.Serialize()
  440. if err != nil {
  441. return nil, err
  442. }
  443. at := archiveType
  444. zipInfoStr := string(zipInfo)
  445. if opts.EncryptFn != nil {
  446. at = archiveTypeEnc
  447. zipInfoStr = string(opts.EncryptFn(archiveTypeEnc, zipInfo))
  448. }
  449. srcInfo.UserDefined[archiveTypeMetadataKey] = at
  450. popts := ObjectOptions{
  451. MTime: srcInfo.ModTime,
  452. VersionID: srcInfo.VersionID,
  453. EvalMetadataFn: func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) {
  454. oi.UserDefined[archiveTypeMetadataKey] = at
  455. oi.UserDefined[archiveInfoMetadataKey] = zipInfoStr
  456. return dsc, nil
  457. },
  458. }
  459. // For all other modes use in-place update to update metadata on a specific version.
  460. if _, err = objectAPI.PutObjectMetadata(ctx, bucket, object, popts); err != nil {
  461. return nil, err
  462. }
  463. return zipInfo, nil
  464. }