Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

maven.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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/sha512"
  8. "encoding/hex"
  9. "encoding/xml"
  10. "errors"
  11. "io"
  12. "net/http"
  13. "path/filepath"
  14. "regexp"
  15. "sort"
  16. "strconv"
  17. "strings"
  18. packages_model "code.gitea.io/gitea/models/packages"
  19. "code.gitea.io/gitea/modules/context"
  20. "code.gitea.io/gitea/modules/json"
  21. "code.gitea.io/gitea/modules/log"
  22. packages_module "code.gitea.io/gitea/modules/packages"
  23. maven_module "code.gitea.io/gitea/modules/packages/maven"
  24. "code.gitea.io/gitea/routers/api/packages/helper"
  25. packages_service "code.gitea.io/gitea/services/packages"
  26. "github.com/minio/sha256-simd"
  27. )
  28. const (
  29. mavenMetadataFile = "maven-metadata.xml"
  30. extensionMD5 = ".md5"
  31. extensionSHA1 = ".sha1"
  32. extensionSHA256 = ".sha256"
  33. extensionSHA512 = ".sha512"
  34. extensionPom = ".pom"
  35. extensionJar = ".jar"
  36. contentTypeJar = "application/java-archive"
  37. contentTypeXML = "text/xml"
  38. )
  39. var (
  40. errInvalidParameters = errors.New("request parameters are invalid")
  41. illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`)
  42. )
  43. func apiError(ctx *context.Context, status int, obj any) {
  44. helper.LogAndProcessError(ctx, status, obj, func(message string) {
  45. // The maven client does not present the error message to the user. Log it for users with access to server logs.
  46. if status == http.StatusBadRequest || status == http.StatusInternalServerError {
  47. log.Error(message)
  48. }
  49. ctx.PlainText(status, message)
  50. })
  51. }
  52. // DownloadPackageFile serves the content of a package
  53. func DownloadPackageFile(ctx *context.Context) {
  54. handlePackageFile(ctx, true)
  55. }
  56. // ProvidePackageFileHeader provides only the headers describing a package
  57. func ProvidePackageFileHeader(ctx *context.Context) {
  58. handlePackageFile(ctx, false)
  59. }
  60. func handlePackageFile(ctx *context.Context, serveContent bool) {
  61. params, err := extractPathParameters(ctx)
  62. if err != nil {
  63. apiError(ctx, http.StatusBadRequest, err)
  64. return
  65. }
  66. if params.IsMeta && params.Version == "" {
  67. serveMavenMetadata(ctx, params)
  68. } else {
  69. servePackageFile(ctx, params, serveContent)
  70. }
  71. }
  72. func serveMavenMetadata(ctx *context.Context, params parameters) {
  73. // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
  74. packageName := params.GroupID + "-" + params.ArtifactID
  75. pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName)
  76. if err != nil {
  77. apiError(ctx, http.StatusInternalServerError, err)
  78. return
  79. }
  80. if len(pvs) == 0 {
  81. apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
  82. return
  83. }
  84. pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
  85. if err != nil {
  86. apiError(ctx, http.StatusInternalServerError, err)
  87. return
  88. }
  89. sort.Slice(pds, func(i, j int) bool {
  90. // Maven and Gradle order packages by their creation timestamp and not by their version string
  91. return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
  92. })
  93. xmlMetadata, err := xml.Marshal(createMetadataResponse(pds))
  94. if err != nil {
  95. apiError(ctx, http.StatusInternalServerError, err)
  96. return
  97. }
  98. xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
  99. latest := pds[len(pds)-1]
  100. ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat))
  101. ext := strings.ToLower(filepath.Ext(params.Filename))
  102. if isChecksumExtension(ext) {
  103. var hash []byte
  104. switch ext {
  105. case extensionMD5:
  106. tmp := md5.Sum(xmlMetadataWithHeader)
  107. hash = tmp[:]
  108. case extensionSHA1:
  109. tmp := sha1.Sum(xmlMetadataWithHeader)
  110. hash = tmp[:]
  111. case extensionSHA256:
  112. tmp := sha256.Sum256(xmlMetadataWithHeader)
  113. hash = tmp[:]
  114. case extensionSHA512:
  115. tmp := sha512.Sum512(xmlMetadataWithHeader)
  116. hash = tmp[:]
  117. }
  118. ctx.PlainText(http.StatusOK, hex.EncodeToString(hash))
  119. return
  120. }
  121. ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
  122. ctx.Resp.Header().Set("Content-Type", contentTypeXML)
  123. if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil {
  124. log.Error("write bytes failed: %v", err)
  125. }
  126. }
  127. func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
  128. packageName := params.GroupID + "-" + params.ArtifactID
  129. pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
  130. if err != nil {
  131. if err == packages_model.ErrPackageNotExist {
  132. apiError(ctx, http.StatusNotFound, err)
  133. } else {
  134. apiError(ctx, http.StatusInternalServerError, err)
  135. }
  136. return
  137. }
  138. filename := params.Filename
  139. ext := strings.ToLower(filepath.Ext(filename))
  140. if isChecksumExtension(ext) {
  141. filename = filename[:len(filename)-len(ext)]
  142. }
  143. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey)
  144. if err != nil {
  145. if err == packages_model.ErrPackageFileNotExist {
  146. apiError(ctx, http.StatusNotFound, err)
  147. } else {
  148. apiError(ctx, http.StatusInternalServerError, err)
  149. }
  150. return
  151. }
  152. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  153. if err != nil {
  154. apiError(ctx, http.StatusInternalServerError, err)
  155. return
  156. }
  157. if isChecksumExtension(ext) {
  158. var hash string
  159. switch ext {
  160. case extensionMD5:
  161. hash = pb.HashMD5
  162. case extensionSHA1:
  163. hash = pb.HashSHA1
  164. case extensionSHA256:
  165. hash = pb.HashSHA256
  166. case extensionSHA512:
  167. hash = pb.HashSHA512
  168. }
  169. ctx.PlainText(http.StatusOK, hash)
  170. return
  171. }
  172. opts := &context.ServeHeaderOptions{
  173. ContentLength: &pb.Size,
  174. LastModified: pf.CreatedUnix.AsLocalTime(),
  175. }
  176. switch ext {
  177. case extensionJar:
  178. opts.ContentType = contentTypeJar
  179. case extensionPom:
  180. opts.ContentType = contentTypeXML
  181. }
  182. if !serveContent {
  183. ctx.SetServeHeaders(opts)
  184. ctx.Status(http.StatusOK)
  185. return
  186. }
  187. s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb)
  188. if err != nil {
  189. apiError(ctx, http.StatusInternalServerError, err)
  190. return
  191. }
  192. opts.Filename = pf.Name
  193. helper.ServePackageFile(ctx, s, u, pf, opts)
  194. }
  195. // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
  196. func UploadPackageFile(ctx *context.Context) {
  197. params, err := extractPathParameters(ctx)
  198. if err != nil {
  199. apiError(ctx, http.StatusBadRequest, err)
  200. return
  201. }
  202. log.Trace("Parameters: %+v", params)
  203. // Ignore the package index /<name>/maven-metadata.xml
  204. if params.IsMeta && params.Version == "" {
  205. ctx.Status(http.StatusOK)
  206. return
  207. }
  208. packageName := params.GroupID + "-" + params.ArtifactID
  209. buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
  210. if err != nil {
  211. apiError(ctx, http.StatusInternalServerError, err)
  212. return
  213. }
  214. defer buf.Close()
  215. pvci := &packages_service.PackageCreationInfo{
  216. PackageInfo: packages_service.PackageInfo{
  217. Owner: ctx.Package.Owner,
  218. PackageType: packages_model.TypeMaven,
  219. Name: packageName,
  220. Version: params.Version,
  221. },
  222. SemverCompatible: false,
  223. Creator: ctx.Doer,
  224. }
  225. ext := filepath.Ext(params.Filename)
  226. // Do not upload checksum files but compare the hashes.
  227. if isChecksumExtension(ext) {
  228. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  229. if err != nil {
  230. if err == packages_model.ErrPackageNotExist {
  231. apiError(ctx, http.StatusNotFound, err)
  232. return
  233. }
  234. apiError(ctx, http.StatusInternalServerError, err)
  235. return
  236. }
  237. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey)
  238. if err != nil {
  239. if err == packages_model.ErrPackageFileNotExist {
  240. apiError(ctx, http.StatusNotFound, err)
  241. return
  242. }
  243. apiError(ctx, http.StatusInternalServerError, err)
  244. return
  245. }
  246. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  247. if err != nil {
  248. apiError(ctx, http.StatusInternalServerError, err)
  249. return
  250. }
  251. hash, err := io.ReadAll(buf)
  252. if err != nil {
  253. apiError(ctx, http.StatusInternalServerError, err)
  254. return
  255. }
  256. if (ext == extensionMD5 && pb.HashMD5 != string(hash)) ||
  257. (ext == extensionSHA1 && pb.HashSHA1 != string(hash)) ||
  258. (ext == extensionSHA256 && pb.HashSHA256 != string(hash)) ||
  259. (ext == extensionSHA512 && pb.HashSHA512 != string(hash)) {
  260. apiError(ctx, http.StatusBadRequest, "hash mismatch")
  261. return
  262. }
  263. ctx.Status(http.StatusOK)
  264. return
  265. }
  266. pfci := &packages_service.PackageFileCreationInfo{
  267. PackageFileInfo: packages_service.PackageFileInfo{
  268. Filename: params.Filename,
  269. },
  270. Creator: ctx.Doer,
  271. Data: buf,
  272. IsLead: false,
  273. OverwriteExisting: params.IsMeta,
  274. }
  275. // If it's the package pom file extract the metadata
  276. if ext == extensionPom {
  277. pfci.IsLead = true
  278. var err error
  279. pvci.Metadata, err = maven_module.ParsePackageMetaData(buf)
  280. if err != nil {
  281. apiError(ctx, http.StatusBadRequest, err)
  282. return
  283. }
  284. if pvci.Metadata != nil {
  285. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  286. if err != nil && err != packages_model.ErrPackageNotExist {
  287. apiError(ctx, http.StatusInternalServerError, err)
  288. return
  289. }
  290. if pv != nil {
  291. raw, err := json.Marshal(pvci.Metadata)
  292. if err != nil {
  293. apiError(ctx, http.StatusInternalServerError, err)
  294. return
  295. }
  296. pv.MetadataJSON = string(raw)
  297. if err := packages_model.UpdateVersion(ctx, pv); err != nil {
  298. apiError(ctx, http.StatusInternalServerError, err)
  299. return
  300. }
  301. }
  302. }
  303. if _, err := buf.Seek(0, io.SeekStart); err != nil {
  304. apiError(ctx, http.StatusInternalServerError, err)
  305. return
  306. }
  307. }
  308. _, _, err = packages_service.CreatePackageOrAddFileToExisting(
  309. ctx,
  310. pvci,
  311. pfci,
  312. )
  313. if err != nil {
  314. switch err {
  315. case packages_model.ErrDuplicatePackageFile:
  316. apiError(ctx, http.StatusBadRequest, err)
  317. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  318. apiError(ctx, http.StatusForbidden, err)
  319. default:
  320. apiError(ctx, http.StatusInternalServerError, err)
  321. }
  322. return
  323. }
  324. ctx.Status(http.StatusCreated)
  325. }
  326. func isChecksumExtension(ext string) bool {
  327. return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
  328. }
  329. type parameters struct {
  330. GroupID string
  331. ArtifactID string
  332. Version string
  333. Filename string
  334. IsMeta bool
  335. }
  336. func extractPathParameters(ctx *context.Context) (parameters, error) {
  337. parts := strings.Split(ctx.Params("*"), "/")
  338. p := parameters{
  339. Filename: parts[len(parts)-1],
  340. }
  341. p.IsMeta = p.Filename == mavenMetadataFile ||
  342. p.Filename == mavenMetadataFile+extensionMD5 ||
  343. p.Filename == mavenMetadataFile+extensionSHA1 ||
  344. p.Filename == mavenMetadataFile+extensionSHA256 ||
  345. p.Filename == mavenMetadataFile+extensionSHA512
  346. parts = parts[:len(parts)-1]
  347. if len(parts) == 0 {
  348. return p, errInvalidParameters
  349. }
  350. p.Version = parts[len(parts)-1]
  351. if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") {
  352. p.Version = ""
  353. } else {
  354. parts = parts[:len(parts)-1]
  355. }
  356. if illegalCharacters.MatchString(p.Version) {
  357. return p, errInvalidParameters
  358. }
  359. if len(parts) < 2 {
  360. return p, errInvalidParameters
  361. }
  362. p.ArtifactID = parts[len(parts)-1]
  363. p.GroupID = strings.Join(parts[:len(parts)-1], ".")
  364. if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) {
  365. return p, errInvalidParameters
  366. }
  367. return p, nil
  368. }