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.

maven.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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/filepath"
  15. "regexp"
  16. "sort"
  17. "strconv"
  18. "strings"
  19. packages_model "code.gitea.io/gitea/models/packages"
  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. "code.gitea.io/gitea/services/context"
  26. packages_service "code.gitea.io/gitea/services/packages"
  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. _, _ = ctx.Resp.Write(xmlMetadataWithHeader)
  124. }
  125. func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
  126. packageName := params.GroupID + "-" + params.ArtifactID
  127. pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
  128. if err != nil {
  129. if err == packages_model.ErrPackageNotExist {
  130. apiError(ctx, http.StatusNotFound, err)
  131. } else {
  132. apiError(ctx, http.StatusInternalServerError, err)
  133. }
  134. return
  135. }
  136. filename := params.Filename
  137. ext := strings.ToLower(filepath.Ext(filename))
  138. if isChecksumExtension(ext) {
  139. filename = filename[:len(filename)-len(ext)]
  140. }
  141. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey)
  142. if err != nil {
  143. if err == packages_model.ErrPackageFileNotExist {
  144. apiError(ctx, http.StatusNotFound, err)
  145. } else {
  146. apiError(ctx, http.StatusInternalServerError, err)
  147. }
  148. return
  149. }
  150. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  151. if err != nil {
  152. apiError(ctx, http.StatusInternalServerError, err)
  153. return
  154. }
  155. if isChecksumExtension(ext) {
  156. var hash string
  157. switch ext {
  158. case extensionMD5:
  159. hash = pb.HashMD5
  160. case extensionSHA1:
  161. hash = pb.HashSHA1
  162. case extensionSHA256:
  163. hash = pb.HashSHA256
  164. case extensionSHA512:
  165. hash = pb.HashSHA512
  166. }
  167. ctx.PlainText(http.StatusOK, hash)
  168. return
  169. }
  170. opts := &context.ServeHeaderOptions{
  171. ContentLength: &pb.Size,
  172. LastModified: pf.CreatedUnix.AsLocalTime(),
  173. }
  174. switch ext {
  175. case extensionJar:
  176. opts.ContentType = contentTypeJar
  177. case extensionPom:
  178. opts.ContentType = contentTypeXML
  179. }
  180. if !serveContent {
  181. ctx.SetServeHeaders(opts)
  182. ctx.Status(http.StatusOK)
  183. return
  184. }
  185. s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb)
  186. if err != nil {
  187. apiError(ctx, http.StatusInternalServerError, err)
  188. return
  189. }
  190. opts.Filename = pf.Name
  191. helper.ServePackageFile(ctx, s, u, pf, opts)
  192. }
  193. // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
  194. func UploadPackageFile(ctx *context.Context) {
  195. params, err := extractPathParameters(ctx)
  196. if err != nil {
  197. apiError(ctx, http.StatusBadRequest, err)
  198. return
  199. }
  200. log.Trace("Parameters: %+v", params)
  201. // Ignore the package index /<name>/maven-metadata.xml
  202. if params.IsMeta && params.Version == "" {
  203. ctx.Status(http.StatusOK)
  204. return
  205. }
  206. packageName := params.GroupID + "-" + params.ArtifactID
  207. buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
  208. if err != nil {
  209. apiError(ctx, http.StatusInternalServerError, err)
  210. return
  211. }
  212. defer buf.Close()
  213. pvci := &packages_service.PackageCreationInfo{
  214. PackageInfo: packages_service.PackageInfo{
  215. Owner: ctx.Package.Owner,
  216. PackageType: packages_model.TypeMaven,
  217. Name: packageName,
  218. Version: params.Version,
  219. },
  220. SemverCompatible: false,
  221. Creator: ctx.Doer,
  222. }
  223. ext := filepath.Ext(params.Filename)
  224. // Do not upload checksum files but compare the hashes.
  225. if isChecksumExtension(ext) {
  226. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  227. if err != nil {
  228. if err == packages_model.ErrPackageNotExist {
  229. apiError(ctx, http.StatusNotFound, err)
  230. return
  231. }
  232. apiError(ctx, http.StatusInternalServerError, err)
  233. return
  234. }
  235. pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey)
  236. if err != nil {
  237. if err == packages_model.ErrPackageFileNotExist {
  238. apiError(ctx, http.StatusNotFound, err)
  239. return
  240. }
  241. apiError(ctx, http.StatusInternalServerError, err)
  242. return
  243. }
  244. pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
  245. if err != nil {
  246. apiError(ctx, http.StatusInternalServerError, err)
  247. return
  248. }
  249. hash, err := io.ReadAll(buf)
  250. if err != nil {
  251. apiError(ctx, http.StatusInternalServerError, err)
  252. return
  253. }
  254. if (ext == extensionMD5 && pb.HashMD5 != string(hash)) ||
  255. (ext == extensionSHA1 && pb.HashSHA1 != string(hash)) ||
  256. (ext == extensionSHA256 && pb.HashSHA256 != string(hash)) ||
  257. (ext == extensionSHA512 && pb.HashSHA512 != string(hash)) {
  258. apiError(ctx, http.StatusBadRequest, "hash mismatch")
  259. return
  260. }
  261. ctx.Status(http.StatusOK)
  262. return
  263. }
  264. pfci := &packages_service.PackageFileCreationInfo{
  265. PackageFileInfo: packages_service.PackageFileInfo{
  266. Filename: params.Filename,
  267. },
  268. Creator: ctx.Doer,
  269. Data: buf,
  270. IsLead: false,
  271. OverwriteExisting: params.IsMeta,
  272. }
  273. // If it's the package pom file extract the metadata
  274. if ext == extensionPom {
  275. pfci.IsLead = true
  276. var err error
  277. pvci.Metadata, err = maven_module.ParsePackageMetaData(buf)
  278. if err != nil {
  279. apiError(ctx, http.StatusBadRequest, err)
  280. return
  281. }
  282. if pvci.Metadata != nil {
  283. pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
  284. if err != nil && err != packages_model.ErrPackageNotExist {
  285. apiError(ctx, http.StatusInternalServerError, err)
  286. return
  287. }
  288. if pv != nil {
  289. raw, err := json.Marshal(pvci.Metadata)
  290. if err != nil {
  291. apiError(ctx, http.StatusInternalServerError, err)
  292. return
  293. }
  294. pv.MetadataJSON = string(raw)
  295. if err := packages_model.UpdateVersion(ctx, pv); err != nil {
  296. apiError(ctx, http.StatusInternalServerError, err)
  297. return
  298. }
  299. }
  300. }
  301. if _, err := buf.Seek(0, io.SeekStart); err != nil {
  302. apiError(ctx, http.StatusInternalServerError, err)
  303. return
  304. }
  305. }
  306. _, _, err = packages_service.CreatePackageOrAddFileToExisting(
  307. ctx,
  308. pvci,
  309. pfci,
  310. )
  311. if err != nil {
  312. switch err {
  313. case packages_model.ErrDuplicatePackageFile:
  314. apiError(ctx, http.StatusConflict, err)
  315. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  316. apiError(ctx, http.StatusForbidden, err)
  317. default:
  318. apiError(ctx, http.StatusInternalServerError, err)
  319. }
  320. return
  321. }
  322. ctx.Status(http.StatusCreated)
  323. }
  324. func isChecksumExtension(ext string) bool {
  325. return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
  326. }
  327. type parameters struct {
  328. GroupID string
  329. ArtifactID string
  330. Version string
  331. Filename string
  332. IsMeta bool
  333. }
  334. func extractPathParameters(ctx *context.Context) (parameters, error) {
  335. parts := strings.Split(ctx.Params("*"), "/")
  336. p := parameters{
  337. Filename: parts[len(parts)-1],
  338. }
  339. p.IsMeta = p.Filename == mavenMetadataFile ||
  340. p.Filename == mavenMetadataFile+extensionMD5 ||
  341. p.Filename == mavenMetadataFile+extensionSHA1 ||
  342. p.Filename == mavenMetadataFile+extensionSHA256 ||
  343. p.Filename == mavenMetadataFile+extensionSHA512
  344. parts = parts[:len(parts)-1]
  345. if len(parts) == 0 {
  346. return p, errInvalidParameters
  347. }
  348. p.Version = parts[len(parts)-1]
  349. if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") {
  350. p.Version = ""
  351. } else {
  352. parts = parts[:len(parts)-1]
  353. }
  354. if illegalCharacters.MatchString(p.Version) {
  355. return p, errInvalidParameters
  356. }
  357. if len(parts) < 2 {
  358. return p, errInvalidParameters
  359. }
  360. p.ArtifactID = parts[len(parts)-1]
  361. p.GroupID = strings.Join(parts[:len(parts)-1], ".")
  362. if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) {
  363. return p, errInvalidParameters
  364. }
  365. return p, nil
  366. }