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