code.gitea.io/gitea@v1.22.3/services/packages/alpine/repository.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package alpine
     5  
     6  import (
     7  	"archive/tar"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"context"
    11  	"crypto"
    12  	"crypto/rand"
    13  	"crypto/rsa"
    14  	"crypto/sha1"
    15  	"crypto/x509"
    16  	"encoding/hex"
    17  	"encoding/pem"
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"strings"
    22  
    23  	packages_model "code.gitea.io/gitea/models/packages"
    24  	alpine_model "code.gitea.io/gitea/models/packages/alpine"
    25  	user_model "code.gitea.io/gitea/models/user"
    26  	"code.gitea.io/gitea/modules/container"
    27  	"code.gitea.io/gitea/modules/json"
    28  	packages_module "code.gitea.io/gitea/modules/packages"
    29  	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
    30  	"code.gitea.io/gitea/modules/util"
    31  	packages_service "code.gitea.io/gitea/services/packages"
    32  )
    33  
    34  const (
    35  	IndexFilename        = "APKINDEX"
    36  	IndexArchiveFilename = IndexFilename + ".tar.gz"
    37  )
    38  
    39  // GetOrCreateRepositoryVersion gets or creates the internal repository package
    40  // The Alpine registry needs multiple index files which are stored in this package.
    41  func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
    42  	return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion)
    43  }
    44  
    45  // GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files
    46  func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
    47  	priv, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPrivate)
    48  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    49  		return "", "", err
    50  	}
    51  
    52  	pub, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPublic)
    53  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    54  		return "", "", err
    55  	}
    56  
    57  	if priv == "" || pub == "" {
    58  		priv, pub, err = util.GenerateKeyPair(4096)
    59  		if err != nil {
    60  			return "", "", err
    61  		}
    62  
    63  		if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPrivate, priv); err != nil {
    64  			return "", "", err
    65  		}
    66  
    67  		if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPublic, pub); err != nil {
    68  			return "", "", err
    69  		}
    70  	}
    71  
    72  	return priv, pub, nil
    73  }
    74  
    75  // BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures
    76  func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
    77  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	// 1. Delete all existing repository files
    83  	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	for _, pf := range pfs {
    89  		if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
    90  			return err
    91  		}
    92  	}
    93  
    94  	// 2. (Re)Build repository files for existing packages
    95  	branches, err := alpine_model.GetBranches(ctx, ownerID)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	for _, branch := range branches {
   100  		repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch)
   101  		if err != nil {
   102  			return err
   103  		}
   104  		for _, repository := range repositories {
   105  			architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
   106  			if err != nil {
   107  				return err
   108  			}
   109  			for _, architecture := range architectures {
   110  				if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
   111  					return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err)
   112  				}
   113  			}
   114  		}
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // BuildSpecificRepositoryFiles builds index files for the repository
   121  func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error {
   122  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	architectures := container.SetOf(architecture)
   128  	if architecture == alpine_module.NoArch {
   129  		// Update all other architectures too when updating the noarch index
   130  		additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
   131  		if err != nil {
   132  			return err
   133  		}
   134  		architectures.AddMultiple(additionalArchitectures...)
   135  	}
   136  
   137  	for architecture := range architectures {
   138  		if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
   139  			return err
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  type packageData struct {
   146  	Package         *packages_model.Package
   147  	Version         *packages_model.PackageVersion
   148  	Blob            *packages_model.PackageBlob
   149  	VersionMetadata *alpine_module.VersionMetadata
   150  	FileMetadata    *alpine_module.FileMetadata
   151  }
   152  
   153  type packageCache = map[*packages_model.PackageFile]*packageData
   154  
   155  func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
   156  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   157  		OwnerID:     ownerID,
   158  		PackageType: packages_model.TypeAlpine,
   159  		Query:       "%.apk",
   160  		Properties: map[string]string{
   161  			alpine_module.PropertyBranch:       branch,
   162  			alpine_module.PropertyRepository:   repository,
   163  			alpine_module.PropertyArchitecture: architecture,
   164  		},
   165  	})
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return pfs, nil
   170  }
   171  
   172  // https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
   173  func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
   174  	pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	if architecture != alpine_module.NoArch {
   179  		// Add all noarch packages too
   180  		noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		pfs = append(pfs, noarchFiles...)
   185  	}
   186  
   187  	// Delete the package indices if there are no packages
   188  	if len(pfs) == 0 {
   189  		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
   190  		if err != nil && !errors.Is(err, util.ErrNotExist) {
   191  			return err
   192  		} else if pf == nil {
   193  			return nil
   194  		}
   195  
   196  		return packages_service.DeletePackageFile(ctx, pf)
   197  	}
   198  
   199  	// Cache data needed for all repository files
   200  	cache := make(packageCache)
   201  	for _, pf := range pfs {
   202  		pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
   203  		if err != nil {
   204  			return err
   205  		}
   206  		p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
   207  		if err != nil {
   208  			return err
   209  		}
   210  		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
   211  		if err != nil {
   212  			return err
   213  		}
   214  		pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata)
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		pd := &packageData{
   220  			Package: p,
   221  			Version: pv,
   222  			Blob:    pb,
   223  		}
   224  
   225  		if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
   226  			return err
   227  		}
   228  		if len(pps) > 0 {
   229  			if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
   230  				return err
   231  			}
   232  		}
   233  
   234  		cache[pf] = pd
   235  	}
   236  
   237  	var buf bytes.Buffer
   238  	for _, pf := range pfs {
   239  		pd := cache[pf]
   240  
   241  		fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
   242  		fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
   243  		fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
   244  		fmt.Fprintf(&buf, "A:%s\n", architecture)
   245  		if pd.VersionMetadata.Description != "" {
   246  			fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
   247  		}
   248  		if pd.VersionMetadata.ProjectURL != "" {
   249  			fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL)
   250  		}
   251  		if pd.VersionMetadata.License != "" {
   252  			fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License)
   253  		}
   254  		fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size)
   255  		fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size)
   256  		fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin)
   257  		fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer)
   258  		fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate)
   259  		if pd.FileMetadata.CommitHash != "" {
   260  			fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash)
   261  		}
   262  		if len(pd.FileMetadata.Dependencies) > 0 {
   263  			fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " "))
   264  		}
   265  		if len(pd.FileMetadata.Provides) > 0 {
   266  			fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " "))
   267  		}
   268  		if pd.FileMetadata.InstallIf != "" {
   269  			fmt.Fprintf(&buf, "i:%s\n", pd.FileMetadata.InstallIf)
   270  		}
   271  		if pd.FileMetadata.ProviderPriority > 0 {
   272  			fmt.Fprintf(&buf, "k:%d\n", pd.FileMetadata.ProviderPriority)
   273  		}
   274  		fmt.Fprint(&buf, "\n")
   275  	}
   276  
   277  	unsignedIndexContent, _ := packages_module.NewHashedBuffer()
   278  	defer unsignedIndexContent.Close()
   279  
   280  	h := sha1.New()
   281  
   282  	if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
   283  		return err
   284  	}
   285  
   286  	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	privPem, _ := pem.Decode([]byte(priv))
   292  	if privPem == nil {
   293  		return fmt.Errorf("failed to decode private key pem")
   294  	}
   295  
   296  	privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil))
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	owner, err := user_model.GetUserByID(ctx, ownerID)
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	signedIndexContent, _ := packages_module.NewHashedBuffer()
   317  	defer signedIndexContent.Close()
   318  
   319  	if err := writeGzipStream(
   320  		signedIndexContent,
   321  		fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)),
   322  		sign,
   323  		false,
   324  	); err != nil {
   325  		return err
   326  	}
   327  
   328  	if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil {
   329  		return err
   330  	}
   331  
   332  	_, err = packages_service.AddFileToPackageVersionInternal(
   333  		ctx,
   334  		repoVersion,
   335  		&packages_service.PackageFileCreationInfo{
   336  			PackageFileInfo: packages_service.PackageFileInfo{
   337  				Filename:     IndexArchiveFilename,
   338  				CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
   339  			},
   340  			Creator:           user_model.NewGhostUser(),
   341  			Data:              signedIndexContent,
   342  			IsLead:            false,
   343  			OverwriteExisting: true,
   344  			Properties: map[string]string{
   345  				alpine_module.PropertyBranch:       branch,
   346  				alpine_module.PropertyRepository:   repository,
   347  				alpine_module.PropertyArchitecture: architecture,
   348  			},
   349  		},
   350  	)
   351  	return err
   352  }
   353  
   354  func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error {
   355  	zw := gzip.NewWriter(w)
   356  	defer zw.Close()
   357  
   358  	tw := tar.NewWriter(zw)
   359  	if addTarEnd {
   360  		defer tw.Close()
   361  	}
   362  	hdr := &tar.Header{
   363  		Name: filename,
   364  		Mode: 0o600,
   365  		Size: int64(len(content)),
   366  	}
   367  	if err := tw.WriteHeader(hdr); err != nil {
   368  		return err
   369  	}
   370  	if _, err := tw.Write(content); err != nil {
   371  		return err
   372  	}
   373  	return nil
   374  }