code.gitea.io/gitea@v1.21.7/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/sha512" 10 "encoding/hex" 11 "encoding/xml" 12 "errors" 13 "io" 14 "net/http" 15 "path/filepath" 16 "regexp" 17 "sort" 18 "strconv" 19 "strings" 20 21 packages_model "code.gitea.io/gitea/models/packages" 22 "code.gitea.io/gitea/modules/context" 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/routers/api/packages/helper" 28 packages_service "code.gitea.io/gitea/services/packages" 29 30 "github.com/minio/sha256-simd" 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 ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat)) 119 120 ext := strings.ToLower(filepath.Ext(params.Filename)) 121 if isChecksumExtension(ext) { 122 var hash []byte 123 switch ext { 124 case extensionMD5: 125 tmp := md5.Sum(xmlMetadataWithHeader) 126 hash = tmp[:] 127 case extensionSHA1: 128 tmp := sha1.Sum(xmlMetadataWithHeader) 129 hash = tmp[:] 130 case extensionSHA256: 131 tmp := sha256.Sum256(xmlMetadataWithHeader) 132 hash = tmp[:] 133 case extensionSHA512: 134 tmp := sha512.Sum512(xmlMetadataWithHeader) 135 hash = tmp[:] 136 } 137 ctx.PlainText(http.StatusOK, hex.EncodeToString(hash)) 138 return 139 } 140 141 ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader))) 142 ctx.Resp.Header().Set("Content-Type", contentTypeXML) 143 144 if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { 145 log.Error("write bytes failed: %v", err) 146 } 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 // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. 230 func UploadPackageFile(ctx *context.Context) { 231 params, err := extractPathParameters(ctx) 232 if err != nil { 233 apiError(ctx, http.StatusBadRequest, err) 234 return 235 } 236 237 log.Trace("Parameters: %+v", params) 238 239 // Ignore the package index /<name>/maven-metadata.xml 240 if params.IsMeta && params.Version == "" { 241 ctx.Status(http.StatusOK) 242 return 243 } 244 245 packageName := params.GroupID + "-" + params.ArtifactID 246 247 buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body) 248 if err != nil { 249 apiError(ctx, http.StatusInternalServerError, err) 250 return 251 } 252 defer buf.Close() 253 254 pvci := &packages_service.PackageCreationInfo{ 255 PackageInfo: packages_service.PackageInfo{ 256 Owner: ctx.Package.Owner, 257 PackageType: packages_model.TypeMaven, 258 Name: packageName, 259 Version: params.Version, 260 }, 261 SemverCompatible: false, 262 Creator: ctx.Doer, 263 } 264 265 ext := filepath.Ext(params.Filename) 266 267 // Do not upload checksum files but compare the hashes. 268 if isChecksumExtension(ext) { 269 pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) 270 if err != nil { 271 if err == packages_model.ErrPackageNotExist { 272 apiError(ctx, http.StatusNotFound, err) 273 return 274 } 275 apiError(ctx, http.StatusInternalServerError, err) 276 return 277 } 278 pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) 279 if err != nil { 280 if err == packages_model.ErrPackageFileNotExist { 281 apiError(ctx, http.StatusNotFound, err) 282 return 283 } 284 apiError(ctx, http.StatusInternalServerError, err) 285 return 286 } 287 pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) 288 if err != nil { 289 apiError(ctx, http.StatusInternalServerError, err) 290 return 291 } 292 293 hash, err := io.ReadAll(buf) 294 if err != nil { 295 apiError(ctx, http.StatusInternalServerError, err) 296 return 297 } 298 299 if (ext == extensionMD5 && pb.HashMD5 != string(hash)) || 300 (ext == extensionSHA1 && pb.HashSHA1 != string(hash)) || 301 (ext == extensionSHA256 && pb.HashSHA256 != string(hash)) || 302 (ext == extensionSHA512 && pb.HashSHA512 != string(hash)) { 303 apiError(ctx, http.StatusBadRequest, "hash mismatch") 304 return 305 } 306 307 ctx.Status(http.StatusOK) 308 return 309 } 310 311 pfci := &packages_service.PackageFileCreationInfo{ 312 PackageFileInfo: packages_service.PackageFileInfo{ 313 Filename: params.Filename, 314 }, 315 Creator: ctx.Doer, 316 Data: buf, 317 IsLead: false, 318 OverwriteExisting: params.IsMeta, 319 } 320 321 // If it's the package pom file extract the metadata 322 if ext == extensionPom { 323 pfci.IsLead = true 324 325 var err error 326 pvci.Metadata, err = maven_module.ParsePackageMetaData(buf) 327 if err != nil { 328 apiError(ctx, http.StatusBadRequest, err) 329 return 330 } 331 332 if pvci.Metadata != nil { 333 pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) 334 if err != nil && err != packages_model.ErrPackageNotExist { 335 apiError(ctx, http.StatusInternalServerError, err) 336 return 337 } 338 if pv != nil { 339 raw, err := json.Marshal(pvci.Metadata) 340 if err != nil { 341 apiError(ctx, http.StatusInternalServerError, err) 342 return 343 } 344 pv.MetadataJSON = string(raw) 345 if err := packages_model.UpdateVersion(ctx, pv); err != nil { 346 apiError(ctx, http.StatusInternalServerError, err) 347 return 348 } 349 } 350 } 351 352 if _, err := buf.Seek(0, io.SeekStart); err != nil { 353 apiError(ctx, http.StatusInternalServerError, err) 354 return 355 } 356 } 357 358 _, _, err = packages_service.CreatePackageOrAddFileToExisting( 359 ctx, 360 pvci, 361 pfci, 362 ) 363 if err != nil { 364 switch err { 365 case packages_model.ErrDuplicatePackageFile: 366 apiError(ctx, http.StatusBadRequest, err) 367 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 368 apiError(ctx, http.StatusForbidden, err) 369 default: 370 apiError(ctx, http.StatusInternalServerError, err) 371 } 372 return 373 } 374 375 ctx.Status(http.StatusCreated) 376 } 377 378 func isChecksumExtension(ext string) bool { 379 return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512 380 } 381 382 type parameters struct { 383 GroupID string 384 ArtifactID string 385 Version string 386 Filename string 387 IsMeta bool 388 } 389 390 func extractPathParameters(ctx *context.Context) (parameters, error) { 391 parts := strings.Split(ctx.Params("*"), "/") 392 393 p := parameters{ 394 Filename: parts[len(parts)-1], 395 } 396 397 p.IsMeta = p.Filename == mavenMetadataFile || 398 p.Filename == mavenMetadataFile+extensionMD5 || 399 p.Filename == mavenMetadataFile+extensionSHA1 || 400 p.Filename == mavenMetadataFile+extensionSHA256 || 401 p.Filename == mavenMetadataFile+extensionSHA512 402 403 parts = parts[:len(parts)-1] 404 if len(parts) == 0 { 405 return p, errInvalidParameters 406 } 407 408 p.Version = parts[len(parts)-1] 409 if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") { 410 p.Version = "" 411 } else { 412 parts = parts[:len(parts)-1] 413 } 414 415 if illegalCharacters.MatchString(p.Version) { 416 return p, errInvalidParameters 417 } 418 419 if len(parts) < 2 { 420 return p, errInvalidParameters 421 } 422 423 p.ArtifactID = parts[len(parts)-1] 424 p.GroupID = strings.Join(parts[:len(parts)-1], ".") 425 426 if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) { 427 return p, errInvalidParameters 428 } 429 430 return p, nil 431 }