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