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  }