gitea/services/packages/rpm/repository.go

603 lines
16 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"net/url"
"strings"
"time"
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/armor"
"github.com/keybase/go-crypto/openpgp/packet"
)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
// The RPM registry needs multiple metadata files which are stored in this package.
func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) {
return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
}
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
pub, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPublic)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
if priv == "" || pub == "" {
priv, pub, err = generateKeypair()
if err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
return "", "", err
}
}
return priv, pub, nil
}
func generateKeypair() (string, string, error) {
e, err := openpgp.NewEntity(setting.AppName, "RPM Registry", "", nil)
if err != nil {
return "", "", err
}
var priv strings.Builder
var pub strings.Builder
w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
if err != nil {
return "", "", err
}
if err := e.SerializePrivate(w, nil); err != nil {
return "", "", err
}
w.Close()
w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
if err != nil {
return "", "", err
}
if err := e.Serialize(w); err != nil {
return "", "", err
}
w.Close()
return priv.String(), pub.String(), nil
}
type repoChecksum struct {
Value string `xml:",chardata"`
Type string `xml:"type,attr"`
}
type repoLocation struct {
Href string `xml:"href,attr"`
}
type repoData struct {
Type string `xml:"type,attr"`
Checksum repoChecksum `xml:"checksum"`
OpenChecksum repoChecksum `xml:"open-checksum"`
Location repoLocation `xml:"location"`
Timestamp int64 `xml:"timestamp"`
Size int64 `xml:"size"`
OpenSize int64 `xml:"open-size"`
}
type packageData struct {
Package *packages_model.Package
Version *packages_model.PackageVersion
Blob *packages_model.PackageBlob
VersionMetadata *rpm_module.VersionMetadata
FileMetadata *rpm_module.FileMetadata
}
type packageCache = map[*packages_model.PackageFile]*packageData
// BuildSpecificRepositoryFiles builds metadata files for the repository
func BuildRepositoryFiles(ctx context.Context, ownerID int64) error {
pv, err := GetOrCreateRepositoryVersion(ownerID)
if err != nil {
return err
}
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
OwnerID: ownerID,
PackageType: packages_model.TypeRpm,
Query: "%.rpm",
})
if err != nil {
return err
}
// Delete the repository files if there are no packages
if len(pfs) == 0 {
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
return err
}
for _, pf := range pfs {
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
return err
}
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
return err
}
}
return nil
}
// Cache data needed for all repository files
cache := make(packageCache)
for _, pf := range pfs {
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
return err
}
p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
if err != nil {
return err
}
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
if err != nil {
return err
}
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
if err != nil {
return err
}
pd := &packageData{
Package: p,
Version: pv,
Blob: pb,
}
if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
return err
}
if len(pps) > 0 {
if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
return err
}
}
cache[pf] = pd
}
primary, err := buildPrimary(pv, pfs, cache)
if err != nil {
return err
}
filelists, err := buildFilelists(pv, pfs, cache)
if err != nil {
return err
}
other, err := buildOther(pv, pfs, cache)
if err != nil {
return err
}
return buildRepomd(
ctx,
pv,
ownerID,
[]*repoData{
primary,
filelists,
other,
},
)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData) error {
type Repomd struct {
XMLName xml.Name `xml:"repomd"`
Xmlns string `xml:"xmlns,attr"`
XmlnsRpm string `xml:"xmlns:rpm,attr"`
Data []*repoData `xml:"data"`
}
var buf bytes.Buffer
buf.WriteString(xml.Header)
if err := xml.NewEncoder(&buf).Encode(&Repomd{
Xmlns: "http://linux.duke.edu/metadata/repo",
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
Data: data,
}); err != nil {
return err
}
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
if err != nil {
return err
}
block, err := armor.Decode(strings.NewReader(priv))
if err != nil {
return err
}
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return err
}
repomdAscContent, _ := packages_module.NewHashedBuffer()
if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
return err
}
repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
for _, file := range []struct {
Name string
Data packages_module.HashedSizeReader
}{
{"repomd.xml", repomdContent},
{"repomd.xml.asc", repomdAscContent},
} {
_, err = packages_service.AddFileToPackageVersionInternal(
pv,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: file.Name,
},
Creator: user_model.NewGhostUser(),
Data: file.Data,
IsLead: false,
OverwriteExisting: true,
},
)
if err != nil {
return err
}
}
return nil
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
func buildPrimary(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) {
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Checksum struct {
Checksum string `xml:",chardata"`
Type string `xml:"type,attr"`
Pkgid string `xml:"pkgid,attr"`
}
type Times struct {
File uint64 `xml:"file,attr"`
Build uint64 `xml:"build,attr"`
}
type Sizes struct {
Package int64 `xml:"package,attr"`
Installed uint64 `xml:"installed,attr"`
Archive uint64 `xml:"archive,attr"`
}
type Location struct {
Href string `xml:"href,attr"`
}
type EntryList struct {
Entries []*rpm_module.Entry `xml:"rpm:entry"`
}
type Format struct {
License string `xml:"rpm:license"`
Vendor string `xml:"rpm:vendor"`
Group string `xml:"rpm:group"`
Buildhost string `xml:"rpm:buildhost"`
Sourcerpm string `xml:"rpm:sourcerpm"`
Provides EntryList `xml:"rpm:provides"`
Requires EntryList `xml:"rpm:requires"`
Conflicts EntryList `xml:"rpm:conflicts"`
Obsoletes EntryList `xml:"rpm:obsoletes"`
Files []*rpm_module.File `xml:"file"`
}
type Package struct {
XMLName xml.Name `xml:"package"`
Type string `xml:"type,attr"`
Name string `xml:"name"`
Architecture string `xml:"arch"`
Version Version `xml:"version"`
Checksum Checksum `xml:"checksum"`
Summary string `xml:"summary"`
Description string `xml:"description"`
Packager string `xml:"packager"`
URL string `xml:"url"`
Time Times `xml:"time"`
Size Sizes `xml:"size"`
Location Location `xml:"location"`
Format Format `xml:"format"`
}
type Metadata struct {
XMLName xml.Name `xml:"metadata"`
Xmlns string `xml:"xmlns,attr"`
XmlnsRpm string `xml:"xmlns:rpm,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
files := make([]*rpm_module.File, 0, 3)
for _, f := range pd.FileMetadata.Files {
if f.IsExecutable {
files = append(files, f)
}
}
packages = append(packages, &Package{
Type: "rpm",
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Checksum: Checksum{
Type: "sha256",
Checksum: pd.Blob.HashSHA256,
Pkgid: "YES",
},
Summary: pd.VersionMetadata.Summary,
Description: pd.VersionMetadata.Description,
Packager: pd.FileMetadata.Packager,
URL: pd.VersionMetadata.ProjectURL,
Time: Times{
File: pd.FileMetadata.FileTime,
Build: pd.FileMetadata.BuildTime,
},
Size: Sizes{
Package: pd.Blob.Size,
Installed: pd.FileMetadata.InstalledSize,
Archive: pd.FileMetadata.ArchiveSize,
},
Location: Location{
Href: fmt.Sprintf("package/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.FileMetadata.Architecture)),
},
Format: Format{
License: pd.VersionMetadata.License,
Vendor: pd.FileMetadata.Vendor,
Group: pd.FileMetadata.Group,
Buildhost: pd.FileMetadata.BuildHost,
Sourcerpm: pd.FileMetadata.SourceRpm,
Provides: EntryList{
Entries: pd.FileMetadata.Provides,
},
Requires: EntryList{
Entries: pd.FileMetadata.Requires,
},
Conflicts: EntryList{
Entries: pd.FileMetadata.Conflicts,
},
Obsoletes: EntryList{
Entries: pd.FileMetadata.Obsoletes,
},
Files: files,
},
})
}
return addDataAsFileToRepo(pv, "primary", &Metadata{
Xmlns: "http://linux.duke.edu/metadata/common",
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
PackageCount: len(pfs),
Packages: packages,
})
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
func buildFilelists(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Package struct {
Pkgid string `xml:"pkgid,attr"`
Name string `xml:"name,attr"`
Architecture string `xml:"arch,attr"`
Version Version `xml:"version"`
Files []*rpm_module.File `xml:"file"`
}
type Filelists struct {
XMLName xml.Name `xml:"filelists"`
Xmlns string `xml:"xmlns,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
packages = append(packages, &Package{
Pkgid: pd.Blob.HashSHA256,
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Files: pd.FileMetadata.Files,
})
}
return addDataAsFileToRepo(pv, "filelists", &Filelists{
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
})
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
func buildOther(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Package struct {
Pkgid string `xml:"pkgid,attr"`
Name string `xml:"name,attr"`
Architecture string `xml:"arch,attr"`
Version Version `xml:"version"`
Changelogs []*rpm_module.Changelog `xml:"changelog"`
}
type Otherdata struct {
XMLName xml.Name `xml:"otherdata"`
Xmlns string `xml:"xmlns,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
packages = append(packages, &Package{
Pkgid: pd.Blob.HashSHA256,
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Changelogs: pd.FileMetadata.Changelogs,
})
}
return addDataAsFileToRepo(pv, "other", &Otherdata{
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
})
}
// writtenCounter counts all written bytes
type writtenCounter struct {
written int64
}
func (wc *writtenCounter) Write(buf []byte) (int, error) {
n := len(buf)
wc.written += int64(n)
return n, nil
}
func (wc *writtenCounter) Written() int64 {
return wc.written
}
func addDataAsFileToRepo(pv *packages_model.PackageVersion, filetype string, obj any) (*repoData, error) {
content, _ := packages_module.NewHashedBuffer()
gzw := gzip.NewWriter(content)
wc := &writtenCounter{}
h := sha256.New()
w := io.MultiWriter(gzw, wc, h)
_, _ = w.Write([]byte(xml.Header))
if err := xml.NewEncoder(w).Encode(obj); err != nil {
return nil, err
}
if err := gzw.Close(); err != nil {
return nil, err
}
filename := filetype + ".xml.gz"
_, err := packages_service.AddFileToPackageVersionInternal(
pv,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
},
Creator: user_model.NewGhostUser(),
Data: content,
IsLead: false,
OverwriteExisting: true,
},
)
if err != nil {
return nil, err
}
_, _, hashSHA256, _ := content.Sums()
return &repoData{
Type: filetype,
Checksum: repoChecksum{
Type: "sha256",
Value: hex.EncodeToString(hashSHA256),
},
OpenChecksum: repoChecksum{
Type: "sha256",
Value: hex.EncodeToString(h.Sum(nil)),
},
Location: repoLocation{
Href: "repodata/" + filename,
},
Timestamp: time.Now().Unix(),
Size: content.Size(),
OpenSize: wc.Written(),
}, nil
}