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  }