code.gitea.io/gitea@v1.21.7/services/packages/rpm/repository.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package rpm
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"encoding/xml"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"net/url"
    17  	"strings"
    18  	"time"
    19  
    20  	packages_model "code.gitea.io/gitea/models/packages"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	"code.gitea.io/gitea/modules/json"
    23  	packages_module "code.gitea.io/gitea/modules/packages"
    24  	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
    25  	"code.gitea.io/gitea/modules/util"
    26  	packages_service "code.gitea.io/gitea/services/packages"
    27  
    28  	"github.com/keybase/go-crypto/openpgp"
    29  	"github.com/keybase/go-crypto/openpgp/armor"
    30  	"github.com/keybase/go-crypto/openpgp/packet"
    31  )
    32  
    33  // GetOrCreateRepositoryVersion gets or creates the internal repository package
    34  // The RPM registry needs multiple metadata files which are stored in this package.
    35  func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
    36  	return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
    37  }
    38  
    39  // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
    40  func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
    41  	priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate)
    42  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    43  		return "", "", err
    44  	}
    45  
    46  	pub, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPublic)
    47  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    48  		return "", "", err
    49  	}
    50  
    51  	if priv == "" || pub == "" {
    52  		priv, pub, err = generateKeypair()
    53  		if err != nil {
    54  			return "", "", err
    55  		}
    56  
    57  		if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
    58  			return "", "", err
    59  		}
    60  
    61  		if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
    62  			return "", "", err
    63  		}
    64  	}
    65  
    66  	return priv, pub, nil
    67  }
    68  
    69  func generateKeypair() (string, string, error) {
    70  	e, err := openpgp.NewEntity("", "RPM Registry", "", nil)
    71  	if err != nil {
    72  		return "", "", err
    73  	}
    74  
    75  	var priv strings.Builder
    76  	var pub strings.Builder
    77  
    78  	w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
    79  	if err != nil {
    80  		return "", "", err
    81  	}
    82  	if err := e.SerializePrivate(w, nil); err != nil {
    83  		return "", "", err
    84  	}
    85  	w.Close()
    86  
    87  	w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
    88  	if err != nil {
    89  		return "", "", err
    90  	}
    91  	if err := e.Serialize(w); err != nil {
    92  		return "", "", err
    93  	}
    94  	w.Close()
    95  
    96  	return priv.String(), pub.String(), nil
    97  }
    98  
    99  type repoChecksum struct {
   100  	Value string `xml:",chardata"`
   101  	Type  string `xml:"type,attr"`
   102  }
   103  
   104  type repoLocation struct {
   105  	Href string `xml:"href,attr"`
   106  }
   107  
   108  type repoData struct {
   109  	Type         string       `xml:"type,attr"`
   110  	Checksum     repoChecksum `xml:"checksum"`
   111  	OpenChecksum repoChecksum `xml:"open-checksum"`
   112  	Location     repoLocation `xml:"location"`
   113  	Timestamp    int64        `xml:"timestamp"`
   114  	Size         int64        `xml:"size"`
   115  	OpenSize     int64        `xml:"open-size"`
   116  }
   117  
   118  type packageData struct {
   119  	Package         *packages_model.Package
   120  	Version         *packages_model.PackageVersion
   121  	Blob            *packages_model.PackageBlob
   122  	VersionMetadata *rpm_module.VersionMetadata
   123  	FileMetadata    *rpm_module.FileMetadata
   124  }
   125  
   126  type packageCache = map[*packages_model.PackageFile]*packageData
   127  
   128  // BuildRepositoryFiles builds metadata files for the repository
   129  func BuildRepositoryFiles(ctx context.Context, ownerID int64) error {
   130  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   136  		OwnerID:     ownerID,
   137  		PackageType: packages_model.TypeRpm,
   138  		Query:       "%.rpm",
   139  	})
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	// Delete the repository files if there are no packages
   145  	if len(pfs) == 0 {
   146  		pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		for _, pf := range pfs {
   151  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   152  				return err
   153  			}
   154  		}
   155  
   156  		return nil
   157  	}
   158  
   159  	// Cache data needed for all repository files
   160  	cache := make(packageCache)
   161  	for _, pf := range pfs {
   162  		pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
   163  		if err != nil {
   164  			return err
   165  		}
   166  		p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
   175  		if err != nil {
   176  			return err
   177  		}
   178  
   179  		pd := &packageData{
   180  			Package: p,
   181  			Version: pv,
   182  			Blob:    pb,
   183  		}
   184  
   185  		if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
   186  			return err
   187  		}
   188  		if len(pps) > 0 {
   189  			if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
   190  				return err
   191  			}
   192  		}
   193  
   194  		cache[pf] = pd
   195  	}
   196  
   197  	primary, err := buildPrimary(ctx, pv, pfs, cache)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	filelists, err := buildFilelists(ctx, pv, pfs, cache)
   202  	if err != nil {
   203  		return err
   204  	}
   205  	other, err := buildOther(ctx, pv, pfs, cache)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	return buildRepomd(
   211  		ctx,
   212  		pv,
   213  		ownerID,
   214  		[]*repoData{
   215  			primary,
   216  			filelists,
   217  			other,
   218  		},
   219  	)
   220  }
   221  
   222  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
   223  func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData) error {
   224  	type Repomd struct {
   225  		XMLName  xml.Name    `xml:"repomd"`
   226  		Xmlns    string      `xml:"xmlns,attr"`
   227  		XmlnsRpm string      `xml:"xmlns:rpm,attr"`
   228  		Data     []*repoData `xml:"data"`
   229  	}
   230  
   231  	var buf bytes.Buffer
   232  	buf.WriteString(xml.Header)
   233  	if err := xml.NewEncoder(&buf).Encode(&Repomd{
   234  		Xmlns:    "http://linux.duke.edu/metadata/repo",
   235  		XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
   236  		Data:     data,
   237  	}); err != nil {
   238  		return err
   239  	}
   240  
   241  	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	block, err := armor.Decode(strings.NewReader(priv))
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	repomdAscContent, _ := packages_module.NewHashedBuffer()
   257  	defer repomdAscContent.Close()
   258  
   259  	if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
   260  		return err
   261  	}
   262  
   263  	repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
   264  	defer repomdContent.Close()
   265  
   266  	for _, file := range []struct {
   267  		Name string
   268  		Data packages_module.HashedSizeReader
   269  	}{
   270  		{"repomd.xml", repomdContent},
   271  		{"repomd.xml.asc", repomdAscContent},
   272  	} {
   273  		_, err = packages_service.AddFileToPackageVersionInternal(
   274  			ctx,
   275  			pv,
   276  			&packages_service.PackageFileCreationInfo{
   277  				PackageFileInfo: packages_service.PackageFileInfo{
   278  					Filename: file.Name,
   279  				},
   280  				Creator:           user_model.NewGhostUser(),
   281  				Data:              file.Data,
   282  				IsLead:            false,
   283  				OverwriteExisting: true,
   284  			},
   285  		)
   286  		if err != nil {
   287  			return err
   288  		}
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
   295  func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) {
   296  	type Version struct {
   297  		Epoch   string `xml:"epoch,attr"`
   298  		Version string `xml:"ver,attr"`
   299  		Release string `xml:"rel,attr"`
   300  	}
   301  
   302  	type Checksum struct {
   303  		Checksum string `xml:",chardata"`
   304  		Type     string `xml:"type,attr"`
   305  		Pkgid    string `xml:"pkgid,attr"`
   306  	}
   307  
   308  	type Times struct {
   309  		File  uint64 `xml:"file,attr"`
   310  		Build uint64 `xml:"build,attr"`
   311  	}
   312  
   313  	type Sizes struct {
   314  		Package   int64  `xml:"package,attr"`
   315  		Installed uint64 `xml:"installed,attr"`
   316  		Archive   uint64 `xml:"archive,attr"`
   317  	}
   318  
   319  	type Location struct {
   320  		Href string `xml:"href,attr"`
   321  	}
   322  
   323  	type EntryList struct {
   324  		Entries []*rpm_module.Entry `xml:"rpm:entry"`
   325  	}
   326  
   327  	type Format struct {
   328  		License   string             `xml:"rpm:license"`
   329  		Vendor    string             `xml:"rpm:vendor"`
   330  		Group     string             `xml:"rpm:group"`
   331  		Buildhost string             `xml:"rpm:buildhost"`
   332  		Sourcerpm string             `xml:"rpm:sourcerpm"`
   333  		Provides  EntryList          `xml:"rpm:provides"`
   334  		Requires  EntryList          `xml:"rpm:requires"`
   335  		Conflicts EntryList          `xml:"rpm:conflicts"`
   336  		Obsoletes EntryList          `xml:"rpm:obsoletes"`
   337  		Files     []*rpm_module.File `xml:"file"`
   338  	}
   339  
   340  	type Package struct {
   341  		XMLName      xml.Name `xml:"package"`
   342  		Type         string   `xml:"type,attr"`
   343  		Name         string   `xml:"name"`
   344  		Architecture string   `xml:"arch"`
   345  		Version      Version  `xml:"version"`
   346  		Checksum     Checksum `xml:"checksum"`
   347  		Summary      string   `xml:"summary"`
   348  		Description  string   `xml:"description"`
   349  		Packager     string   `xml:"packager"`
   350  		URL          string   `xml:"url"`
   351  		Time         Times    `xml:"time"`
   352  		Size         Sizes    `xml:"size"`
   353  		Location     Location `xml:"location"`
   354  		Format       Format   `xml:"format"`
   355  	}
   356  
   357  	type Metadata struct {
   358  		XMLName      xml.Name   `xml:"metadata"`
   359  		Xmlns        string     `xml:"xmlns,attr"`
   360  		XmlnsRpm     string     `xml:"xmlns:rpm,attr"`
   361  		PackageCount int        `xml:"packages,attr"`
   362  		Packages     []*Package `xml:"package"`
   363  	}
   364  
   365  	packages := make([]*Package, 0, len(pfs))
   366  	for _, pf := range pfs {
   367  		pd := c[pf]
   368  
   369  		files := make([]*rpm_module.File, 0, 3)
   370  		for _, f := range pd.FileMetadata.Files {
   371  			if f.IsExecutable {
   372  				files = append(files, f)
   373  			}
   374  		}
   375  
   376  		packages = append(packages, &Package{
   377  			Type:         "rpm",
   378  			Name:         pd.Package.Name,
   379  			Architecture: pd.FileMetadata.Architecture,
   380  			Version: Version{
   381  				Epoch:   pd.FileMetadata.Epoch,
   382  				Version: pd.FileMetadata.Version,
   383  				Release: pd.FileMetadata.Release,
   384  			},
   385  			Checksum: Checksum{
   386  				Type:     "sha256",
   387  				Checksum: pd.Blob.HashSHA256,
   388  				Pkgid:    "YES",
   389  			},
   390  			Summary:     pd.VersionMetadata.Summary,
   391  			Description: pd.VersionMetadata.Description,
   392  			Packager:    pd.FileMetadata.Packager,
   393  			URL:         pd.VersionMetadata.ProjectURL,
   394  			Time: Times{
   395  				File:  pd.FileMetadata.FileTime,
   396  				Build: pd.FileMetadata.BuildTime,
   397  			},
   398  			Size: Sizes{
   399  				Package:   pd.Blob.Size,
   400  				Installed: pd.FileMetadata.InstalledSize,
   401  				Archive:   pd.FileMetadata.ArchiveSize,
   402  			},
   403  			Location: Location{
   404  				Href: fmt.Sprintf("package/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.FileMetadata.Architecture)),
   405  			},
   406  			Format: Format{
   407  				License:   pd.VersionMetadata.License,
   408  				Vendor:    pd.FileMetadata.Vendor,
   409  				Group:     pd.FileMetadata.Group,
   410  				Buildhost: pd.FileMetadata.BuildHost,
   411  				Sourcerpm: pd.FileMetadata.SourceRpm,
   412  				Provides: EntryList{
   413  					Entries: pd.FileMetadata.Provides,
   414  				},
   415  				Requires: EntryList{
   416  					Entries: pd.FileMetadata.Requires,
   417  				},
   418  				Conflicts: EntryList{
   419  					Entries: pd.FileMetadata.Conflicts,
   420  				},
   421  				Obsoletes: EntryList{
   422  					Entries: pd.FileMetadata.Obsoletes,
   423  				},
   424  				Files: files,
   425  			},
   426  		})
   427  	}
   428  
   429  	return addDataAsFileToRepo(ctx, pv, "primary", &Metadata{
   430  		Xmlns:        "http://linux.duke.edu/metadata/common",
   431  		XmlnsRpm:     "http://linux.duke.edu/metadata/rpm",
   432  		PackageCount: len(pfs),
   433  		Packages:     packages,
   434  	})
   435  }
   436  
   437  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
   438  func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
   439  	type Version struct {
   440  		Epoch   string `xml:"epoch,attr"`
   441  		Version string `xml:"ver,attr"`
   442  		Release string `xml:"rel,attr"`
   443  	}
   444  
   445  	type Package struct {
   446  		Pkgid        string             `xml:"pkgid,attr"`
   447  		Name         string             `xml:"name,attr"`
   448  		Architecture string             `xml:"arch,attr"`
   449  		Version      Version            `xml:"version"`
   450  		Files        []*rpm_module.File `xml:"file"`
   451  	}
   452  
   453  	type Filelists struct {
   454  		XMLName      xml.Name   `xml:"filelists"`
   455  		Xmlns        string     `xml:"xmlns,attr"`
   456  		PackageCount int        `xml:"packages,attr"`
   457  		Packages     []*Package `xml:"package"`
   458  	}
   459  
   460  	packages := make([]*Package, 0, len(pfs))
   461  	for _, pf := range pfs {
   462  		pd := c[pf]
   463  
   464  		packages = append(packages, &Package{
   465  			Pkgid:        pd.Blob.HashSHA256,
   466  			Name:         pd.Package.Name,
   467  			Architecture: pd.FileMetadata.Architecture,
   468  			Version: Version{
   469  				Epoch:   pd.FileMetadata.Epoch,
   470  				Version: pd.FileMetadata.Version,
   471  				Release: pd.FileMetadata.Release,
   472  			},
   473  			Files: pd.FileMetadata.Files,
   474  		})
   475  	}
   476  
   477  	return addDataAsFileToRepo(ctx, pv, "filelists", &Filelists{
   478  		Xmlns:        "http://linux.duke.edu/metadata/other",
   479  		PackageCount: len(pfs),
   480  		Packages:     packages,
   481  	})
   482  }
   483  
   484  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
   485  func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
   486  	type Version struct {
   487  		Epoch   string `xml:"epoch,attr"`
   488  		Version string `xml:"ver,attr"`
   489  		Release string `xml:"rel,attr"`
   490  	}
   491  
   492  	type Package struct {
   493  		Pkgid        string                  `xml:"pkgid,attr"`
   494  		Name         string                  `xml:"name,attr"`
   495  		Architecture string                  `xml:"arch,attr"`
   496  		Version      Version                 `xml:"version"`
   497  		Changelogs   []*rpm_module.Changelog `xml:"changelog"`
   498  	}
   499  
   500  	type Otherdata struct {
   501  		XMLName      xml.Name   `xml:"otherdata"`
   502  		Xmlns        string     `xml:"xmlns,attr"`
   503  		PackageCount int        `xml:"packages,attr"`
   504  		Packages     []*Package `xml:"package"`
   505  	}
   506  
   507  	packages := make([]*Package, 0, len(pfs))
   508  	for _, pf := range pfs {
   509  		pd := c[pf]
   510  
   511  		packages = append(packages, &Package{
   512  			Pkgid:        pd.Blob.HashSHA256,
   513  			Name:         pd.Package.Name,
   514  			Architecture: pd.FileMetadata.Architecture,
   515  			Version: Version{
   516  				Epoch:   pd.FileMetadata.Epoch,
   517  				Version: pd.FileMetadata.Version,
   518  				Release: pd.FileMetadata.Release,
   519  			},
   520  			Changelogs: pd.FileMetadata.Changelogs,
   521  		})
   522  	}
   523  
   524  	return addDataAsFileToRepo(ctx, pv, "other", &Otherdata{
   525  		Xmlns:        "http://linux.duke.edu/metadata/other",
   526  		PackageCount: len(pfs),
   527  		Packages:     packages,
   528  	})
   529  }
   530  
   531  // writtenCounter counts all written bytes
   532  type writtenCounter struct {
   533  	written int64
   534  }
   535  
   536  func (wc *writtenCounter) Write(buf []byte) (int, error) {
   537  	n := len(buf)
   538  
   539  	wc.written += int64(n)
   540  
   541  	return n, nil
   542  }
   543  
   544  func (wc *writtenCounter) Written() int64 {
   545  	return wc.written
   546  }
   547  
   548  func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any) (*repoData, error) {
   549  	content, _ := packages_module.NewHashedBuffer()
   550  	defer content.Close()
   551  
   552  	gzw := gzip.NewWriter(content)
   553  	wc := &writtenCounter{}
   554  	h := sha256.New()
   555  
   556  	w := io.MultiWriter(gzw, wc, h)
   557  	_, _ = w.Write([]byte(xml.Header))
   558  
   559  	if err := xml.NewEncoder(w).Encode(obj); err != nil {
   560  		return nil, err
   561  	}
   562  
   563  	if err := gzw.Close(); err != nil {
   564  		return nil, err
   565  	}
   566  
   567  	filename := filetype + ".xml.gz"
   568  
   569  	_, err := packages_service.AddFileToPackageVersionInternal(
   570  		ctx,
   571  		pv,
   572  		&packages_service.PackageFileCreationInfo{
   573  			PackageFileInfo: packages_service.PackageFileInfo{
   574  				Filename: filename,
   575  			},
   576  			Creator:           user_model.NewGhostUser(),
   577  			Data:              content,
   578  			IsLead:            false,
   579  			OverwriteExisting: true,
   580  		},
   581  	)
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  
   586  	_, _, hashSHA256, _ := content.Sums()
   587  
   588  	return &repoData{
   589  		Type: filetype,
   590  		Checksum: repoChecksum{
   591  			Type:  "sha256",
   592  			Value: hex.EncodeToString(hashSHA256),
   593  		},
   594  		OpenChecksum: repoChecksum{
   595  			Type:  "sha256",
   596  			Value: hex.EncodeToString(h.Sum(nil)),
   597  		},
   598  		Location: repoLocation{
   599  			Href: "repodata/" + filename,
   600  		},
   601  		Timestamp: time.Now().Unix(),
   602  		Size:      content.Size(),
   603  		OpenSize:  wc.Written(),
   604  	}, nil
   605  }