gitea源码

maven.go 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package maven
  4. import (
  5. "crypto/md5"
  6. "crypto/sha1"
  7. "crypto/sha256"
  8. "crypto/sha512"
  9. "encoding/hex"
  10. "encoding/xml"
  11. "errors"
  12. "io"
  13. "net/http"
  14. "path"
  15. "regexp"
  16. "sort"
  17. "strconv"
  18. "strings"
  19. packages_model "code.gitea.io/gitea/models/packages"
  20. "code.gitea.io/gitea/modules/globallock"
  21. "code.gitea.io/gitea/modules/json"
  22. packages_module "code.gitea.io/gitea/modules/packages"
  23. maven_module "code.gitea.io/gitea/modules/packages/maven"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/routers/api/packages/helper"
  26. "code.gitea.io/gitea/services/context"
  27. packages_service "code.gitea.io/gitea/services/packages"
  28. )
  29. const (
  30. mavenMetadataFile = "maven-metadata.xml"
  31. extensionMD5 = ".md5"
  32. extensionSHA1 = ".sha1"
  33. extensionSHA256 = ".sha256"
  34. extensionSHA512 = ".sha512"
  35. extensionPom = ".pom"
  36. extensionJar = ".jar"
  37. contentTypeJar = "application/java-archive"
  38. contentTypeXML = "text/xml"
  39. )
  40. var (
  41. errInvalidParameters = errors.New("request parameters are invalid")
  42. illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`)
  43. )
  44. func apiError(ctx *context.Context, status int, obj any) {
  45. message := helper.ProcessErrorForUser(ctx, status, obj)
  46. // Maven client doesn't present the error message to end users; site admin can check the server logs that outputted by ProcessErrorForUser
  47. ctx.PlainText(status, message)
  48. }
  49. // DownloadPackageFile serves the content of a package
  50. func DownloadPackageFile(ctx *context.Context) {
  51. handlePackageFile(ctx, true)
  52. }
  53. // ProvidePackageFileHeader provides only the headers describing a package
  54. func ProvidePackageFileHeader(ctx *context.Context) {
  55. handlePackageFile(ctx, false)
  56. }
  57. func handlePackageFile(ctx *context.Context, serveContent bool) {
  58. params, err := extractPathParameters(ctx)
  59. if err != nil {
  60. apiError(ctx, http.StatusBadRequest, err)
  61. return
  62. }
  63. if params.IsMeta && params.Version == "" {
  64. serveMavenMetadata(ctx, params)
  65. } else {
  66. servePackageFile(ctx, params, serveContent)
  67. }
  68. }
  69. func serveMavenMetadata(ctx *context.Context, params parameters) {
  70. // path pattern: /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
  71. // in case there are legacy package names ("GroupID-ArtifactID") we need to check both, new packages always use ":" as separator("GroupID:ArtifactID")
  72. pvsLegacy, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
  73. if err != nil {
  74. apiError(ctx, http.StatusInternalServerError, err)
  75. return
  76. }
  77. pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName())
  78. if err != nil {
  79. apiError(ctx, http.StatusInternalServerError, err)
  80. return
  81. }
  82. pvs = append(pvsLegacy, pvs...)
  83. if len(pvs) == 0 {
  84. apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
  85. return
  86. }
  87. pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
  88. if err != nil {
  89. apiError(ctx, http.StatusInternalServerError, err)
  90. return
  91. }
  92. sort.Slice(pds, func(i, j int) bool {
  93. // Maven and Gradle order packages by their creation timestamp and not by their version string
  94. return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
  95. })
  96. xmlMetadata, err := xml.Marshal(createMetadataResponse(pds, params.GroupID, params.ArtifactID))
  97. if err != nil {
  98. apiError(ctx, http.StatusInternalServerError, err)
  99. return
  100. }
  101. xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
  102. latest := pds[len(pds)-1]
  103. // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
  104. lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat)
  105. ctx.Resp.Header().Set("Last-Modified", lastModified)
  106. ext := strings.ToLower(path.Ext(params.Filename))
  107. if isChecksumExtension(ext) {
  108. var hash []byte
  109. switch ext {
  110. case extensionMD5:
  111. tmp := md5.Sum(xmlMetadataWithHeader)
  112. hash = tmp[:]
  113. case extensionSHA1:
  114. tmp := sha1.Sum(xmlMetadataWithHeader)
  115. hash = tmp[:]
  116. case extensionSHA256:
  117. tmp := sha256.Sum256(xmlMetadataWithHeader)
  118. hash = tmp[:]
  119. case extensionSHA512:
  120. tmp := sha512.Sum512(xmlMetadataWithHeader)
  121. hash = tmp[:]
  122. }
  123. ctx.PlainText(http.StatusOK, hex.EncodeToString(hash))
  124. return
  125. }
  126. ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
  127. ctx.Resp.Header().Set("Content-Type", contentTypeXML)
  128. _, _ = ctx.Resp.Write(xmlMetadataWithHeader)
  129. }
  130. func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
  131. pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version)
  132. if errors.Is(err, util.ErrNotExist) {
  133. pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version)
  134. }
  135. if err != nil {
  136. if errors.Is(err, packages_model.ErrPackageNotExist) {
  137. apiError(ctx, http.StatusNotFound, err)
  138. } else {
  139. apiError(ctx, http.StatusInternalServerError, err)
  140. }
  141. return
  142. }
  143. filename := params.Filename
  144. ext := strings.ToLower(path.Ext(filename))
  145. if isChecksumExtension(ext) {
  146. filename = filename[:len(filename)-len(ext)]
  147. }
  148. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey)
  149. if err != nil {
  150. if errors.Is(err, packages_model.ErrPackageFileNotExist) {
  151. apiError(ctx, http.StatusNotFound, err)
  152. } else {
  153. apiError(ctx, http.StatusInternalServerError, err)
  154. }
  155. return
  156. }
  157. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  158. if err != nil {
  159. apiError(ctx, http.StatusInternalServerError, err)
  160. return
  161. }
  162. if isChecksumExtension(ext) {
  163. var hash string
  164. switch ext {
  165. case extensionMD5:
  166. hash = pb.HashMD5
  167. case extensionSHA1:
  168. hash = pb.HashSHA1
  169. case extensionSHA256:
  170. hash = pb.HashSHA256
  171. case extensionSHA512:
  172. hash = pb.HashSHA512
  173. }
  174. ctx.PlainText(http.StatusOK, hash)
  175. return
  176. }
  177. opts := &context.ServeHeaderOptions{
  178. ContentLength: &pb.Size,
  179. LastModified: pf.CreatedUnix.AsLocalTime(),
  180. }
  181. switch ext {
  182. case extensionJar:
  183. opts.ContentType = contentTypeJar
  184. case extensionPom:
  185. opts.ContentType = contentTypeXML
  186. }
  187. if !serveContent {
  188. ctx.SetServeHeaders(opts)
  189. ctx.Status(http.StatusOK)
  190. return
  191. }
  192. s, u, _, err := packages_service.OpenBlobForDownload(ctx, pf, pb, ctx.Req.Method, nil)
  193. if err != nil {
  194. apiError(ctx, http.StatusInternalServerError, err)
  195. return
  196. }
  197. opts.Filename = pf.Name
  198. helper.ServePackageFile(ctx, s, u, pf, opts)
  199. }
  200. func mavenPkgNameKey(packageName string) string {
  201. return "pkg_maven_" + packageName
  202. }
  203. // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
  204. func UploadPackageFile(ctx *context.Context) {
  205. params, err := extractPathParameters(ctx)
  206. if err != nil {
  207. apiError(ctx, http.StatusBadRequest, err)
  208. return
  209. }
  210. // Ignore the package index /<name>/maven-metadata.xml
  211. if params.IsMeta && params.Version == "" {
  212. ctx.Status(http.StatusOK)
  213. return
  214. }
  215. packageName := params.toInternalPackageName()
  216. if ctx.FormBool("use_legacy_package_name") {
  217. // for testing purpose only
  218. packageName = params.toInternalPackageNameLegacy()
  219. }
  220. // for the same package, only one upload at a time
  221. releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName))
  222. if err != nil {
  223. apiError(ctx, http.StatusInternalServerError, err)
  224. return
  225. }
  226. defer releaser()
  227. buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
  228. if err != nil {
  229. apiError(ctx, http.StatusInternalServerError, err)
  230. return
  231. }
  232. defer buf.Close()
  233. pvci := &packages_service.PackageCreationInfo{
  234. PackageInfo: packages_service.PackageInfo{
  235. Owner: ctx.Package.Owner,
  236. PackageType: packages_model.TypeMaven,
  237. Name: packageName,
  238. Version: params.Version,
  239. },
  240. SemverCompatible: false,
  241. Creator: ctx.Doer,
  242. }
  243. // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId"
  244. legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
  245. if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
  246. apiError(ctx, http.StatusInternalServerError, err)
  247. return
  248. } else if legacyPackage != nil {
  249. err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName)
  250. if err != nil {
  251. apiError(ctx, http.StatusInternalServerError, err)
  252. return
  253. }
  254. }
  255. ext := path.Ext(params.Filename)
  256. // Do not upload checksum files but compare the hashes.
  257. if isChecksumExtension(ext) {
  258. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  259. if err != nil {
  260. if errors.Is(err, packages_model.ErrPackageNotExist) {
  261. apiError(ctx, http.StatusNotFound, err)
  262. return
  263. }
  264. apiError(ctx, http.StatusInternalServerError, err)
  265. return
  266. }
  267. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey)
  268. if err != nil {
  269. if errors.Is(err, packages_model.ErrPackageFileNotExist) {
  270. apiError(ctx, http.StatusNotFound, err)
  271. return
  272. }
  273. apiError(ctx, http.StatusInternalServerError, err)
  274. return
  275. }
  276. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  277. if err != nil {
  278. apiError(ctx, http.StatusInternalServerError, err)
  279. return
  280. }
  281. hash, err := io.ReadAll(buf)
  282. if err != nil {
  283. apiError(ctx, http.StatusInternalServerError, err)
  284. return
  285. }
  286. if (ext == extensionMD5 && pb.HashMD5 != string(hash)) ||
  287. (ext == extensionSHA1 && pb.HashSHA1 != string(hash)) ||
  288. (ext == extensionSHA256 && pb.HashSHA256 != string(hash)) ||
  289. (ext == extensionSHA512 && pb.HashSHA512 != string(hash)) {
  290. apiError(ctx, http.StatusBadRequest, "hash mismatch")
  291. return
  292. }
  293. ctx.Status(http.StatusOK)
  294. return
  295. }
  296. pfci := &packages_service.PackageFileCreationInfo{
  297. PackageFileInfo: packages_service.PackageFileInfo{
  298. Filename: params.Filename,
  299. },
  300. Creator: ctx.Doer,
  301. Data: buf,
  302. IsLead: false,
  303. OverwriteExisting: params.IsMeta,
  304. }
  305. // If it's the package pom file extract the metadata
  306. if ext == extensionPom {
  307. pfci.IsLead = true
  308. var err error
  309. pvci.Metadata, err = maven_module.ParsePackageMetaData(buf)
  310. if err != nil {
  311. apiError(ctx, http.StatusBadRequest, err)
  312. return
  313. }
  314. if pvci.Metadata != nil {
  315. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  316. if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
  317. apiError(ctx, http.StatusInternalServerError, err)
  318. return
  319. }
  320. if pv != nil {
  321. raw, err := json.Marshal(pvci.Metadata)
  322. if err != nil {
  323. apiError(ctx, http.StatusInternalServerError, err)
  324. return
  325. }
  326. pv.MetadataJSON = string(raw)
  327. if err := packages_model.UpdateVersion(ctx, pv); err != nil {
  328. apiError(ctx, http.StatusInternalServerError, err)
  329. return
  330. }
  331. }
  332. }
  333. if _, err := buf.Seek(0, io.SeekStart); err != nil {
  334. apiError(ctx, http.StatusInternalServerError, err)
  335. return
  336. }
  337. }
  338. _, _, err = packages_service.CreatePackageOrAddFileToExisting(
  339. ctx,
  340. pvci,
  341. pfci,
  342. )
  343. if err != nil {
  344. switch err {
  345. case packages_model.ErrDuplicatePackageFile:
  346. apiError(ctx, http.StatusConflict, err)
  347. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  348. apiError(ctx, http.StatusForbidden, err)
  349. default:
  350. apiError(ctx, http.StatusInternalServerError, err)
  351. }
  352. return
  353. }
  354. ctx.Status(http.StatusCreated)
  355. }
  356. func isChecksumExtension(ext string) bool {
  357. return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
  358. }
  359. type parameters struct {
  360. GroupID string
  361. ArtifactID string
  362. Version string
  363. Filename string
  364. IsMeta bool
  365. }
  366. func (p *parameters) toInternalPackageName() string {
  367. // there cuold be 2 choices: "/" or ":"
  368. // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates
  369. // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id"
  370. return p.GroupID + ":" + p.ArtifactID
  371. }
  372. func (p *parameters) toInternalPackageNameLegacy() string {
  373. return p.GroupID + "-" + p.ArtifactID
  374. }
  375. func extractPathParameters(ctx *context.Context) (parameters, error) {
  376. parts := strings.Split(ctx.PathParam("*"), "/")
  377. // formats:
  378. // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512]
  379. // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512]
  380. // * /com/group/id/artifactId/version/any-file
  381. // * /com/group/id/artifactId/version-SNAPSHOT/any-file
  382. p := parameters{
  383. Filename: parts[len(parts)-1],
  384. }
  385. p.IsMeta = p.Filename == mavenMetadataFile ||
  386. p.Filename == mavenMetadataFile+extensionMD5 ||
  387. p.Filename == mavenMetadataFile+extensionSHA1 ||
  388. p.Filename == mavenMetadataFile+extensionSHA256 ||
  389. p.Filename == mavenMetadataFile+extensionSHA512
  390. parts = parts[:len(parts)-1]
  391. if len(parts) == 0 {
  392. return p, errInvalidParameters
  393. }
  394. p.Version = parts[len(parts)-1]
  395. if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") {
  396. p.Version = ""
  397. } else {
  398. parts = parts[:len(parts)-1]
  399. }
  400. if illegalCharacters.MatchString(p.Version) {
  401. return p, errInvalidParameters
  402. }
  403. if len(parts) < 2 {
  404. return p, errInvalidParameters
  405. }
  406. p.ArtifactID = parts[len(parts)-1]
  407. p.GroupID = strings.Join(parts[:len(parts)-1], ".")
  408. if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) {
  409. return p, errInvalidParameters
  410. }
  411. return p, nil
  412. }