// Copyright 2021 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package maven import ( "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/xml" "errors" "fmt" "io" "net/http" "path/filepath" "regexp" "strings" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" ) const ( mavenMetadataFile = "maven-metadata.xml" extensionMD5 = ".md5" extensionSHA1 = ".sha1" extensionSHA256 = ".sha256" extensionSHA512 = ".sha512" ) var ( errInvalidParameters = errors.New("request parameters are invalid") illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) ) func apiError(ctx *context.Context, status int, obj interface{}) { helper.LogAndProcessError(ctx, status, obj, func(message string) { ctx.PlainText(status, message) }) } // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { params, err := extractPathParameters(ctx) if err != nil { apiError(ctx, http.StatusBadRequest, err) return } if params.IsMeta && params.Version == "" { serveMavenMetadata(ctx, params) } else { servePackageFile(ctx, params) } } func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] packageName := params.GroupID + "-" + params.ArtifactID pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if len(pvs) == 0 { apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist) return } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) ext := strings.ToLower(filepath.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { case extensionMD5: tmp := md5.Sum(xmlMetadataWithHeader) hash = tmp[:] case extensionSHA1: tmp := sha1.Sum(xmlMetadataWithHeader) hash = tmp[:] case extensionSHA256: tmp := sha256.Sum256(xmlMetadataWithHeader) hash = tmp[:] case extensionSHA512: tmp := sha512.Sum512(xmlMetadataWithHeader) hash = tmp[:] } ctx.PlainText(http.StatusOK, fmt.Sprintf("%x", hash)) return } ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader) } func servePackageFile(ctx *context.Context, params parameters) { packageName := params.GroupID + "-" + params.ArtifactID pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) if err != nil { if err == packages_model.ErrPackageNotExist { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) } return } filename := params.Filename ext := strings.ToLower(filepath.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { if err == packages_model.ErrPackageFileNotExist { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) } return } pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if isChecksumExtension(ext) { var hash string switch ext { case extensionMD5: hash = pb.HashMD5 case extensionSHA1: hash = pb.HashSHA1 case extensionSHA256: hash = pb.HashSHA256 case extensionSHA512: hash = pb.HashSHA512 } ctx.PlainText(http.StatusOK, hash) return } s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) } defer s.Close() if pf.IsLead { if err := packages_model.IncrementDownloadCounter(ctx, pv.ID); err != nil { log.Error("Error incrementing download counter: %v", err) } } ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) } // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. func UploadPackageFile(ctx *context.Context) { params, err := extractPathParameters(ctx) if err != nil { apiError(ctx, http.StatusBadRequest, err) return } log.Trace("Parameters: %+v", params) // Ignore the package index /<name>/maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } packageName := params.GroupID + "-" + params.ArtifactID buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body, 32*1024*1024) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } defer buf.Close() pvci := &packages_service.PackageCreationInfo{ PackageInfo: packages_service.PackageInfo{ Owner: ctx.Package.Owner, PackageType: packages_model.TypeMaven, Name: packageName, Version: params.Version, }, SemverCompatible: false, Creator: ctx.Doer, } ext := filepath.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { if err == packages_model.ErrPackageNotExist { apiError(ctx, http.StatusNotFound, err) return } apiError(ctx, http.StatusInternalServerError, err) return } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { if err == packages_model.ErrPackageFileNotExist { apiError(ctx, http.StatusNotFound, err) return } apiError(ctx, http.StatusInternalServerError, err) return } pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } hash, err := io.ReadAll(buf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if (ext == extensionMD5 && pb.HashMD5 != string(hash)) || (ext == extensionSHA1 && pb.HashSHA1 != string(hash)) || (ext == extensionSHA256 && pb.HashSHA256 != string(hash)) || (ext == extensionSHA512 && pb.HashSHA512 != string(hash)) { apiError(ctx, http.StatusBadRequest, "hash mismatch") return } ctx.Status(http.StatusOK) return } pfci := &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ Filename: params.Filename, }, Data: buf, IsLead: false, OverwriteExisting: params.IsMeta, } // If it's the package pom file extract the metadata if ext == ".pom" { pfci.IsLead = true var err error pvci.Metadata, err = maven_module.ParsePackageMetaData(buf) if err != nil { log.Error("Error parsing package metadata: %v", err) } if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil && err != packages_model.ErrPackageNotExist { apiError(ctx, http.StatusInternalServerError, err) return } if pv != nil { raw, err := json.Marshal(pvci.Metadata) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } pv.MetadataJSON = string(raw) if err := packages_model.UpdateVersion(ctx, pv); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } } } if _, err := buf.Seek(0, io.SeekStart); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } } _, _, err = packages_service.CreatePackageOrAddFileToExisting( pvci, pfci, ) if err != nil { if err == packages_model.ErrDuplicatePackageFile { apiError(ctx, http.StatusBadRequest, err) return } apiError(ctx, http.StatusInternalServerError, err) return } ctx.Status(http.StatusCreated) } func isChecksumExtension(ext string) bool { return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512 } type parameters struct { GroupID string ArtifactID string Version string Filename string IsMeta bool } func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.Params("*"), "/") p := parameters{ Filename: parts[len(parts)-1], } p.IsMeta = p.Filename == mavenMetadataFile || p.Filename == mavenMetadataFile+extensionMD5 || p.Filename == mavenMetadataFile+extensionSHA1 || p.Filename == mavenMetadataFile+extensionSHA256 || p.Filename == mavenMetadataFile+extensionSHA512 parts = parts[:len(parts)-1] if len(parts) == 0 { return p, errInvalidParameters } p.Version = parts[len(parts)-1] if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") { p.Version = "" } else { parts = parts[:len(parts)-1] } if illegalCharacters.MatchString(p.Version) { return p, errInvalidParameters } if len(parts) < 2 { return p, errInvalidParameters } p.ArtifactID = parts[len(parts)-1] p.GroupID = strings.Join(parts[:len(parts)-1], ".") if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) { return p, errInvalidParameters } return p, nil }