gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package container
  4. import (
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. auth_model "code.gitea.io/gitea/models/auth"
  16. packages_model "code.gitea.io/gitea/models/packages"
  17. container_model "code.gitea.io/gitea/models/packages/container"
  18. user_model "code.gitea.io/gitea/models/user"
  19. "code.gitea.io/gitea/modules/httplib"
  20. "code.gitea.io/gitea/modules/json"
  21. "code.gitea.io/gitea/modules/log"
  22. "code.gitea.io/gitea/modules/optional"
  23. packages_module "code.gitea.io/gitea/modules/packages"
  24. container_module "code.gitea.io/gitea/modules/packages/container"
  25. "code.gitea.io/gitea/modules/setting"
  26. "code.gitea.io/gitea/modules/util"
  27. "code.gitea.io/gitea/routers/api/packages/helper"
  28. auth_service "code.gitea.io/gitea/services/auth"
  29. "code.gitea.io/gitea/services/context"
  30. packages_service "code.gitea.io/gitea/services/packages"
  31. container_service "code.gitea.io/gitea/services/packages/container"
  32. "github.com/opencontainers/go-digest"
  33. )
  34. // maximum size of a container manifest
  35. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
  36. const maxManifestSize = 10 * 1024 * 1024
  37. var globalVars = sync.OnceValue(func() (ret struct {
  38. imageNamePattern, referencePattern *regexp.Regexp
  39. },
  40. ) {
  41. ret.imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
  42. ret.referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
  43. return ret
  44. })
  45. type containerHeaders struct {
  46. Status int
  47. ContentDigest string
  48. UploadUUID string
  49. Range string
  50. Location string
  51. ContentType string
  52. ContentLength optional.Option[int64]
  53. }
  54. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
  55. func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
  56. if h.Location != "" {
  57. resp.Header().Set("Location", h.Location)
  58. }
  59. if h.Range != "" {
  60. resp.Header().Set("Range", h.Range)
  61. }
  62. if h.ContentType != "" {
  63. resp.Header().Set("Content-Type", h.ContentType)
  64. }
  65. if h.ContentLength.Has() {
  66. resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10))
  67. }
  68. if h.UploadUUID != "" {
  69. resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
  70. }
  71. if h.ContentDigest != "" {
  72. resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
  73. resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
  74. }
  75. resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
  76. resp.WriteHeader(h.Status)
  77. }
  78. func jsonResponse(ctx *context.Context, status int, obj any) {
  79. setResponseHeaders(ctx.Resp, &containerHeaders{
  80. Status: status,
  81. ContentType: "application/json",
  82. })
  83. _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors
  84. }
  85. func apiError(ctx *context.Context, status int, err error) {
  86. _ = helper.ProcessErrorForUser(ctx, status, err)
  87. setResponseHeaders(ctx.Resp, &containerHeaders{
  88. Status: status,
  89. })
  90. }
  91. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
  92. func apiErrorDefined(ctx *context.Context, err *namedError) {
  93. type ContainerError struct {
  94. Code string `json:"code"`
  95. Message string `json:"message"`
  96. }
  97. type ContainerErrors struct {
  98. Errors []ContainerError `json:"errors"`
  99. }
  100. jsonResponse(ctx, err.StatusCode, ContainerErrors{
  101. Errors: []ContainerError{
  102. {
  103. Code: err.Code,
  104. Message: err.Message,
  105. },
  106. },
  107. })
  108. }
  109. func apiUnauthorizedError(ctx *context.Context) {
  110. // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
  111. realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
  112. ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
  113. apiErrorDefined(ctx, errUnauthorized)
  114. }
  115. // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
  116. func ReqContainerAccess(ctx *context.Context) {
  117. if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
  118. apiUnauthorizedError(ctx)
  119. }
  120. }
  121. // VerifyImageName is a middleware which checks if the image name is allowed
  122. func VerifyImageName(ctx *context.Context) {
  123. if !globalVars().imageNamePattern.MatchString(ctx.PathParam("image")) {
  124. apiErrorDefined(ctx, errNameInvalid)
  125. }
  126. }
  127. // DetermineSupport is used to test if the registry supports OCI
  128. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
  129. func DetermineSupport(ctx *context.Context) {
  130. setResponseHeaders(ctx.Resp, &containerHeaders{
  131. Status: http.StatusOK,
  132. })
  133. }
  134. // Authenticate creates a token for the current user
  135. // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
  136. func Authenticate(ctx *context.Context) {
  137. u := ctx.Doer
  138. packageScope := auth_service.GetAccessScope(ctx.Data)
  139. if u == nil {
  140. if setting.Service.RequireSignInViewStrict {
  141. apiUnauthorizedError(ctx)
  142. return
  143. }
  144. u = user_model.NewGhostUser()
  145. } else {
  146. if has, err := packageScope.HasAnyScope(
  147. auth_model.AccessTokenScopeReadPackage,
  148. auth_model.AccessTokenScopeWritePackage,
  149. auth_model.AccessTokenScopeAll,
  150. ); !has {
  151. if err != nil {
  152. log.Error("Error checking access scope: %v", err)
  153. }
  154. apiUnauthorizedError(ctx)
  155. return
  156. }
  157. }
  158. token, err := packages_service.CreateAuthorizationToken(u, packageScope)
  159. if err != nil {
  160. apiError(ctx, http.StatusInternalServerError, err)
  161. return
  162. }
  163. ctx.JSON(http.StatusOK, map[string]string{
  164. "token": token,
  165. })
  166. }
  167. // https://distribution.github.io/distribution/spec/auth/oauth/
  168. func AuthenticateNotImplemented(ctx *context.Context) {
  169. // This optional endpoint can be used to authenticate a client.
  170. // It must implement the specification described in:
  171. // https://datatracker.ietf.org/doc/html/rfc6749
  172. // https://distribution.github.io/distribution/spec/auth/oauth/
  173. // Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
  174. ctx.Status(http.StatusNotFound)
  175. }
  176. // https://docs.docker.com/registry/spec/api/#listing-repositories
  177. func GetRepositoryList(ctx *context.Context) {
  178. n := ctx.FormInt("n")
  179. if n <= 0 || n > 100 {
  180. n = 100
  181. }
  182. last := ctx.FormTrim("last")
  183. repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
  184. if err != nil {
  185. apiError(ctx, http.StatusInternalServerError, err)
  186. return
  187. }
  188. type RepositoryList struct {
  189. Repositories []string `json:"repositories"`
  190. }
  191. if len(repositories) == n {
  192. v := url.Values{}
  193. if n > 0 {
  194. v.Add("n", strconv.Itoa(n)) // FIXME: "n" can't be zero here, the logic is inconsistent with GetTagsList
  195. }
  196. v.Add("last", repositories[len(repositories)-1])
  197. ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
  198. }
  199. jsonResponse(ctx, http.StatusOK, RepositoryList{
  200. Repositories: repositories,
  201. })
  202. }
  203. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
  204. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
  205. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  206. func PostBlobsUploads(ctx *context.Context) {
  207. image := ctx.PathParam("image")
  208. mount := ctx.FormTrim("mount")
  209. from := ctx.FormTrim("from")
  210. if mount != "" {
  211. blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
  212. Repository: from,
  213. Digest: mount,
  214. })
  215. if blob != nil {
  216. accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
  217. if err != nil {
  218. apiError(ctx, http.StatusInternalServerError, err)
  219. return
  220. }
  221. if accessible {
  222. if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
  223. apiError(ctx, http.StatusInternalServerError, err)
  224. return
  225. }
  226. setResponseHeaders(ctx.Resp, &containerHeaders{
  227. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
  228. ContentDigest: mount,
  229. Status: http.StatusCreated,
  230. })
  231. return
  232. }
  233. }
  234. }
  235. digest := ctx.FormTrim("digest")
  236. if digest != "" {
  237. buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
  238. if err != nil {
  239. apiError(ctx, http.StatusInternalServerError, err)
  240. return
  241. }
  242. defer buf.Close()
  243. if digest != digestFromHashSummer(buf) {
  244. apiErrorDefined(ctx, errDigestInvalid)
  245. return
  246. }
  247. if _, err := saveAsPackageBlob(ctx,
  248. buf,
  249. &packages_service.PackageCreationInfo{
  250. PackageInfo: packages_service.PackageInfo{
  251. Owner: ctx.Package.Owner,
  252. Name: image,
  253. },
  254. Creator: ctx.Doer,
  255. },
  256. ); err != nil {
  257. switch err {
  258. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  259. apiError(ctx, http.StatusForbidden, err)
  260. default:
  261. apiError(ctx, http.StatusInternalServerError, err)
  262. }
  263. return
  264. }
  265. setResponseHeaders(ctx.Resp, &containerHeaders{
  266. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
  267. ContentDigest: digest,
  268. Status: http.StatusCreated,
  269. })
  270. return
  271. }
  272. upload, err := packages_model.CreateBlobUpload(ctx)
  273. if err != nil {
  274. apiError(ctx, http.StatusInternalServerError, err)
  275. return
  276. }
  277. setResponseHeaders(ctx.Resp, &containerHeaders{
  278. Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
  279. UploadUUID: upload.ID,
  280. Status: http.StatusAccepted,
  281. })
  282. }
  283. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  284. func GetBlobsUpload(ctx *context.Context) {
  285. image := ctx.PathParam("image")
  286. uuid := ctx.PathParam("uuid")
  287. upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
  288. if err != nil {
  289. if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
  290. apiErrorDefined(ctx, errBlobUploadUnknown)
  291. } else {
  292. apiError(ctx, http.StatusInternalServerError, err)
  293. }
  294. return
  295. }
  296. // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
  297. respHeaders := &containerHeaders{
  298. Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
  299. UploadUUID: upload.ID,
  300. Status: http.StatusNoContent,
  301. }
  302. if upload.BytesReceived > 0 {
  303. respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1)
  304. }
  305. setResponseHeaders(ctx.Resp, respHeaders)
  306. }
  307. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
  308. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  309. func PatchBlobsUpload(ctx *context.Context) {
  310. image := ctx.PathParam("image")
  311. uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
  312. if err != nil {
  313. if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
  314. apiErrorDefined(ctx, errBlobUploadUnknown)
  315. } else {
  316. apiError(ctx, http.StatusInternalServerError, err)
  317. }
  318. return
  319. }
  320. defer uploader.Close()
  321. contentRange := ctx.Req.Header.Get("Content-Range")
  322. if contentRange != "" {
  323. start, end := 0, 0
  324. if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
  325. apiErrorDefined(ctx, errBlobUploadInvalid)
  326. return
  327. }
  328. if int64(start) != uploader.Size() {
  329. apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
  330. return
  331. }
  332. } else if uploader.Size() != 0 {
  333. apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
  334. return
  335. }
  336. if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
  337. apiError(ctx, http.StatusInternalServerError, err)
  338. return
  339. }
  340. respHeaders := &containerHeaders{
  341. Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
  342. UploadUUID: uploader.ID,
  343. Status: http.StatusAccepted,
  344. }
  345. if uploader.Size() > 0 {
  346. respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
  347. }
  348. setResponseHeaders(ctx.Resp, respHeaders)
  349. }
  350. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  351. func PutBlobsUpload(ctx *context.Context) {
  352. image := ctx.PathParam("image")
  353. digest := ctx.FormTrim("digest")
  354. if digest == "" {
  355. apiErrorDefined(ctx, errDigestInvalid)
  356. return
  357. }
  358. uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
  359. if err != nil {
  360. if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
  361. apiErrorDefined(ctx, errBlobUploadUnknown)
  362. } else {
  363. apiError(ctx, http.StatusInternalServerError, err)
  364. }
  365. return
  366. }
  367. defer uploader.Close()
  368. if ctx.Req.Body != nil {
  369. if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
  370. apiError(ctx, http.StatusInternalServerError, err)
  371. return
  372. }
  373. }
  374. if digest != digestFromHashSummer(uploader) {
  375. apiErrorDefined(ctx, errDigestInvalid)
  376. return
  377. }
  378. if _, err := saveAsPackageBlob(ctx,
  379. uploader,
  380. &packages_service.PackageCreationInfo{
  381. PackageInfo: packages_service.PackageInfo{
  382. Owner: ctx.Package.Owner,
  383. Name: image,
  384. },
  385. Creator: ctx.Doer,
  386. },
  387. ); err != nil {
  388. switch err {
  389. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  390. apiError(ctx, http.StatusForbidden, err)
  391. default:
  392. apiError(ctx, http.StatusInternalServerError, err)
  393. }
  394. return
  395. }
  396. // Some SDK (e.g.: minio) will close the Reader if it is also a Closer after "uploading".
  397. // And we don't need to wrap the reader to anything else because the SDK will benefit from other interfaces like Seeker.
  398. // It's safe to call Close twice, so ignore the error.
  399. _ = uploader.Close()
  400. if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
  401. apiError(ctx, http.StatusInternalServerError, err)
  402. return
  403. }
  404. setResponseHeaders(ctx.Resp, &containerHeaders{
  405. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
  406. ContentDigest: digest,
  407. Status: http.StatusCreated,
  408. })
  409. }
  410. // https://docs.docker.com/registry/spec/api/#delete-blob-upload
  411. func DeleteBlobsUpload(ctx *context.Context) {
  412. uuid := ctx.PathParam("uuid")
  413. _, err := packages_model.GetBlobUploadByID(ctx, uuid)
  414. if err != nil {
  415. if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
  416. apiErrorDefined(ctx, errBlobUploadUnknown)
  417. } else {
  418. apiError(ctx, http.StatusInternalServerError, err)
  419. }
  420. return
  421. }
  422. if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
  423. apiError(ctx, http.StatusInternalServerError, err)
  424. return
  425. }
  426. setResponseHeaders(ctx.Resp, &containerHeaders{
  427. Status: http.StatusNoContent,
  428. })
  429. }
  430. func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
  431. d := digest.Digest(ctx.PathParam("digest"))
  432. if d.Validate() != nil {
  433. return nil, container_model.ErrContainerBlobNotExist
  434. }
  435. return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
  436. OwnerID: ctx.Package.Owner.ID,
  437. Image: ctx.PathParam("image"),
  438. Digest: string(d),
  439. })
  440. }
  441. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
  442. func HeadBlob(ctx *context.Context) {
  443. blob, err := getBlobFromContext(ctx)
  444. if err != nil {
  445. if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  446. apiErrorDefined(ctx, errBlobUnknown)
  447. } else {
  448. apiError(ctx, http.StatusInternalServerError, err)
  449. }
  450. return
  451. }
  452. setResponseHeaders(ctx.Resp, &containerHeaders{
  453. ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
  454. ContentLength: optional.Some(blob.Blob.Size),
  455. Status: http.StatusOK,
  456. })
  457. }
  458. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
  459. func GetBlob(ctx *context.Context) {
  460. blob, err := getBlobFromContext(ctx)
  461. if err != nil {
  462. if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  463. apiErrorDefined(ctx, errBlobUnknown)
  464. } else {
  465. apiError(ctx, http.StatusInternalServerError, err)
  466. }
  467. return
  468. }
  469. serveBlob(ctx, blob)
  470. }
  471. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
  472. func DeleteBlob(ctx *context.Context) {
  473. d := digest.Digest(ctx.PathParam("digest"))
  474. if d.Validate() != nil {
  475. apiErrorDefined(ctx, errBlobUnknown)
  476. return
  477. }
  478. if err := deleteBlob(ctx, ctx.Package.Owner.ID, ctx.PathParam("image"), d); err != nil {
  479. apiError(ctx, http.StatusInternalServerError, err)
  480. return
  481. }
  482. setResponseHeaders(ctx.Resp, &containerHeaders{
  483. Status: http.StatusAccepted,
  484. })
  485. }
  486. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
  487. func PutManifest(ctx *context.Context) {
  488. reference := ctx.PathParam("reference")
  489. mci := &manifestCreationInfo{
  490. MediaType: ctx.Req.Header.Get("Content-Type"),
  491. Owner: ctx.Package.Owner,
  492. Creator: ctx.Doer,
  493. Image: ctx.PathParam("image"),
  494. Reference: reference,
  495. IsTagged: digest.Digest(reference).Validate() != nil,
  496. }
  497. if mci.IsTagged && !globalVars().referencePattern.MatchString(reference) {
  498. apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
  499. return
  500. }
  501. maxSize := maxManifestSize + 1
  502. buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
  503. if err != nil {
  504. apiError(ctx, http.StatusInternalServerError, err)
  505. return
  506. }
  507. defer buf.Close()
  508. if buf.Size() > maxManifestSize {
  509. apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
  510. return
  511. }
  512. digest, err := processManifest(ctx, mci, buf)
  513. if err != nil {
  514. var namedError *namedError
  515. if errors.As(err, &namedError) {
  516. apiErrorDefined(ctx, namedError)
  517. } else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  518. apiErrorDefined(ctx, errBlobUnknown)
  519. } else {
  520. switch err {
  521. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  522. apiError(ctx, http.StatusForbidden, err)
  523. default:
  524. apiError(ctx, http.StatusInternalServerError, err)
  525. }
  526. }
  527. return
  528. }
  529. setResponseHeaders(ctx.Resp, &containerHeaders{
  530. Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
  531. ContentDigest: digest,
  532. Status: http.StatusCreated,
  533. })
  534. }
  535. func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
  536. opts := &container_model.BlobSearchOptions{
  537. OwnerID: ctx.Package.Owner.ID,
  538. Image: ctx.PathParam("image"),
  539. IsManifest: true,
  540. }
  541. reference := ctx.PathParam("reference")
  542. if d := digest.Digest(reference); d.Validate() == nil {
  543. opts.Digest = string(d)
  544. } else if globalVars().referencePattern.MatchString(reference) {
  545. opts.Tag = reference
  546. opts.OnlyLead = true
  547. } else {
  548. return nil, container_model.ErrContainerBlobNotExist
  549. }
  550. return opts, nil
  551. }
  552. func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
  553. opts, err := getBlobSearchOptionsFromContext(ctx)
  554. if err != nil {
  555. return nil, err
  556. }
  557. return workaroundGetContainerBlob(ctx, opts)
  558. }
  559. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
  560. func HeadManifest(ctx *context.Context) {
  561. manifest, err := getManifestFromContext(ctx)
  562. if err != nil {
  563. if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  564. apiErrorDefined(ctx, errManifestUnknown)
  565. } else {
  566. apiError(ctx, http.StatusInternalServerError, err)
  567. }
  568. return
  569. }
  570. setResponseHeaders(ctx.Resp, &containerHeaders{
  571. ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
  572. ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
  573. ContentLength: optional.Some(manifest.Blob.Size),
  574. Status: http.StatusOK,
  575. })
  576. }
  577. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
  578. func GetManifest(ctx *context.Context) {
  579. manifest, err := getManifestFromContext(ctx)
  580. if err != nil {
  581. if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  582. apiErrorDefined(ctx, errManifestUnknown)
  583. } else {
  584. apiError(ctx, http.StatusInternalServerError, err)
  585. }
  586. return
  587. }
  588. serveBlob(ctx, manifest)
  589. }
  590. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
  591. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
  592. func DeleteManifest(ctx *context.Context) {
  593. opts, err := getBlobSearchOptionsFromContext(ctx)
  594. if err != nil {
  595. apiErrorDefined(ctx, errManifestUnknown)
  596. return
  597. }
  598. pvs, err := container_model.GetManifestVersions(ctx, opts)
  599. if err != nil {
  600. apiError(ctx, http.StatusInternalServerError, err)
  601. return
  602. }
  603. if len(pvs) == 0 {
  604. apiErrorDefined(ctx, errManifestUnknown)
  605. return
  606. }
  607. for _, pv := range pvs {
  608. if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
  609. apiError(ctx, http.StatusInternalServerError, err)
  610. return
  611. }
  612. }
  613. setResponseHeaders(ctx.Resp, &containerHeaders{
  614. Status: http.StatusAccepted,
  615. })
  616. }
  617. func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
  618. serveDirectReqParams := make(url.Values)
  619. serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
  620. s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, serveDirectReqParams)
  621. if err != nil {
  622. apiError(ctx, http.StatusInternalServerError, err)
  623. return
  624. }
  625. headers := &containerHeaders{
  626. ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
  627. ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
  628. ContentLength: optional.Some(pfd.Blob.Size),
  629. Status: http.StatusOK,
  630. }
  631. if u != nil {
  632. headers.Status = http.StatusTemporaryRedirect
  633. headers.Location = u.String()
  634. headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses
  635. setResponseHeaders(ctx.Resp, headers)
  636. return
  637. }
  638. defer s.Close()
  639. setResponseHeaders(ctx.Resp, headers)
  640. _, _ = io.Copy(ctx.Resp, s)
  641. }
  642. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
  643. func GetTagsList(ctx *context.Context) {
  644. image := ctx.PathParam("image")
  645. if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
  646. if errors.Is(err, packages_model.ErrPackageNotExist) {
  647. apiErrorDefined(ctx, errNameUnknown)
  648. } else {
  649. apiError(ctx, http.StatusInternalServerError, err)
  650. }
  651. return
  652. }
  653. n := -1
  654. if ctx.FormTrim("n") != "" {
  655. n = ctx.FormInt("n")
  656. }
  657. last := ctx.FormTrim("last")
  658. tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
  659. if err != nil {
  660. apiError(ctx, http.StatusInternalServerError, err)
  661. return
  662. }
  663. type TagList struct {
  664. Name string `json:"name"`
  665. Tags []string `json:"tags"`
  666. }
  667. if len(tags) > 0 {
  668. v := url.Values{}
  669. if n > 0 {
  670. v.Add("n", strconv.Itoa(n))
  671. }
  672. v.Add("last", tags[len(tags)-1])
  673. ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
  674. }
  675. jsonResponse(ctx, http.StatusOK, TagList{
  676. Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
  677. Tags: tags,
  678. })
  679. }
  680. // FIXME: Workaround to be removed in v1.20.
  681. // Update maybe we should never really remote it, as long as there is legacy data?
  682. // https://github.com/go-gitea/gitea/issues/19586
  683. func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
  684. blob, err := container_model.GetContainerBlob(ctx, opts)
  685. if err != nil {
  686. return nil, err
  687. }
  688. err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
  689. if err != nil {
  690. if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
  691. log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
  692. return nil, container_model.ErrContainerBlobNotExist
  693. }
  694. return nil, err
  695. }
  696. return blob, nil
  697. }