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

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package debian
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	packages_model "code.gitea.io/gitea/models/packages"
    18  	debian_model "code.gitea.io/gitea/models/packages/debian"
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	packages_module "code.gitea.io/gitea/modules/packages"
    21  	debian_module "code.gitea.io/gitea/modules/packages/debian"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/util"
    24  	packages_service "code.gitea.io/gitea/services/packages"
    25  
    26  	"github.com/keybase/go-crypto/openpgp"
    27  	"github.com/keybase/go-crypto/openpgp/armor"
    28  	"github.com/keybase/go-crypto/openpgp/clearsign"
    29  	"github.com/keybase/go-crypto/openpgp/packet"
    30  	"github.com/ulikunitz/xz"
    31  )
    32  
    33  // GetOrCreateRepositoryVersion gets or creates the internal repository package
    34  // The Debian registry needs multiple index 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.TypeDebian, debian_module.RepositoryPackage, debian_module.RepositoryVersion)
    37  }
    38  
    39  // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
    40  func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
    41  	priv, err := user_model.GetSetting(ctx, ownerID, debian_module.SettingKeyPrivate)
    42  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    43  		return "", "", err
    44  	}
    45  
    46  	pub, err := user_model.GetSetting(ctx, ownerID, debian_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, debian_module.SettingKeyPrivate, priv); err != nil {
    58  			return "", "", err
    59  		}
    60  
    61  		if err := user_model.SetUserSetting(ctx, ownerID, debian_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("", "Debian 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  // BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures
   100  func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
   101  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	// 1. Delete all existing repository files
   107  	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	for _, pf := range pfs {
   113  		if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   114  			return err
   115  		}
   116  	}
   117  
   118  	// 2. (Re)Build repository files for existing packages
   119  	distributions, err := debian_model.GetDistributions(ctx, ownerID)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	for _, distribution := range distributions {
   124  		components, err := debian_model.GetComponents(ctx, ownerID, distribution)
   125  		if err != nil {
   126  			return err
   127  		}
   128  		architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
   129  		if err != nil {
   130  			return err
   131  		}
   132  
   133  		for _, component := range components {
   134  			for _, architecture := range architectures {
   135  				if err := buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture); err != nil {
   136  					return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", distribution, component, architecture, err)
   137  				}
   138  			}
   139  		}
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  // BuildSpecificRepositoryFiles builds index files for the repository
   146  func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, distribution, component, architecture string) error {
   147  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	return buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture)
   153  }
   154  
   155  func buildRepositoryFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
   156  	if err := buildPackagesIndices(ctx, ownerID, repoVersion, distribution, component, architecture); err != nil {
   157  		return err
   158  	}
   159  
   160  	return buildReleaseFiles(ctx, ownerID, repoVersion, distribution)
   161  }
   162  
   163  // https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
   164  func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
   165  	pfds, err := debian_model.SearchLatestPackages(ctx, &debian_model.PackageSearchOptions{
   166  		OwnerID:      ownerID,
   167  		Distribution: distribution,
   168  		Component:    component,
   169  		Architecture: architecture,
   170  	})
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	// Delete the package indices if there are no packages
   176  	if len(pfds) == 0 {
   177  		key := fmt.Sprintf("%s|%s|%s", distribution, component, architecture)
   178  		for _, filename := range []string{"Packages", "Packages.gz", "Packages.xz"} {
   179  			pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
   180  			if err != nil && !errors.Is(err, util.ErrNotExist) {
   181  				return err
   182  			} else if pf == nil {
   183  				continue
   184  			}
   185  
   186  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   187  				return err
   188  			}
   189  		}
   190  
   191  		return nil
   192  	}
   193  
   194  	packagesContent, _ := packages_module.NewHashedBuffer()
   195  	defer packagesContent.Close()
   196  
   197  	packagesGzipContent, _ := packages_module.NewHashedBuffer()
   198  	defer packagesGzipContent.Close()
   199  
   200  	gzw := gzip.NewWriter(packagesGzipContent)
   201  
   202  	packagesXzContent, _ := packages_module.NewHashedBuffer()
   203  	defer packagesXzContent.Close()
   204  
   205  	xzw, _ := xz.NewWriter(packagesXzContent)
   206  
   207  	w := io.MultiWriter(packagesContent, gzw, xzw)
   208  
   209  	addSeparator := false
   210  	for _, pfd := range pfds {
   211  		if addSeparator {
   212  			fmt.Fprintln(w)
   213  		}
   214  		addSeparator = true
   215  
   216  		fmt.Fprintf(w, "%s\n", strings.TrimSpace(pfd.Properties.GetByName(debian_module.PropertyControl)))
   217  
   218  		fmt.Fprintf(w, "Filename: pool/%s/%s/%s\n", distribution, component, pfd.File.Name)
   219  		fmt.Fprintf(w, "Size: %d\n", pfd.Blob.Size)
   220  		fmt.Fprintf(w, "MD5sum: %s\n", pfd.Blob.HashMD5)
   221  		fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1)
   222  		fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256)
   223  		fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512)
   224  	}
   225  
   226  	gzw.Close()
   227  	xzw.Close()
   228  
   229  	for _, file := range []struct {
   230  		Name string
   231  		Data packages_module.HashedSizeReader
   232  	}{
   233  		{"Packages", packagesContent},
   234  		{"Packages.gz", packagesGzipContent},
   235  		{"Packages.xz", packagesXzContent},
   236  	} {
   237  		_, err = packages_service.AddFileToPackageVersionInternal(
   238  			ctx,
   239  			repoVersion,
   240  			&packages_service.PackageFileCreationInfo{
   241  				PackageFileInfo: packages_service.PackageFileInfo{
   242  					Filename:     file.Name,
   243  					CompositeKey: fmt.Sprintf("%s|%s|%s", distribution, component, architecture),
   244  				},
   245  				Creator:           user_model.NewGhostUser(),
   246  				Data:              file.Data,
   247  				IsLead:            false,
   248  				OverwriteExisting: true,
   249  				Properties: map[string]string{
   250  					debian_module.PropertyRepositoryIncludeInRelease: "",
   251  					debian_module.PropertyDistribution:               distribution,
   252  					debian_module.PropertyComponent:                  component,
   253  					debian_module.PropertyArchitecture:               architecture,
   254  				},
   255  			},
   256  		)
   257  		if err != nil {
   258  			return err
   259  		}
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  // https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
   266  func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution string) error {
   267  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   268  		VersionID: repoVersion.ID,
   269  		Properties: map[string]string{
   270  			debian_module.PropertyRepositoryIncludeInRelease: "",
   271  			debian_module.PropertyDistribution:               distribution,
   272  		},
   273  	})
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	// Delete the release files if there are no packages
   279  	if len(pfs) == 0 {
   280  		for _, filename := range []string{"Release", "Release.gpg", "InRelease"} {
   281  			pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
   282  			if err != nil && !errors.Is(err, util.ErrNotExist) {
   283  				return err
   284  			} else if pf == nil {
   285  				continue
   286  			}
   287  
   288  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   289  				return err
   290  			}
   291  		}
   292  
   293  		return nil
   294  	}
   295  
   296  	components, err := debian_model.GetComponents(ctx, ownerID, distribution)
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	sort.Strings(components)
   302  
   303  	architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	sort.Strings(architectures)
   309  
   310  	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	block, err := armor.Decode(strings.NewReader(priv))
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	inReleaseContent, _ := packages_module.NewHashedBuffer()
   326  	defer inReleaseContent.Close()
   327  
   328  	sw, err := clearsign.Encode(inReleaseContent, e.PrivateKey, nil)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	var buf bytes.Buffer
   334  
   335  	w := io.MultiWriter(sw, &buf)
   336  
   337  	fmt.Fprintf(w, "Origin: %s\n", setting.AppName)
   338  	fmt.Fprintf(w, "Label: %s\n", setting.AppName)
   339  	fmt.Fprintf(w, "Suite: %s\n", distribution)
   340  	fmt.Fprintf(w, "Codename: %s\n", distribution)
   341  	fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
   342  	fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
   343  	fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
   344  	fmt.Fprint(w, "Acquire-By-Hash: yes\n")
   345  
   346  	pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
   347  	if err != nil {
   348  		return err
   349  	}
   350  
   351  	var md5, sha1, sha256, sha512 strings.Builder
   352  	for _, pfd := range pfds {
   353  		path := fmt.Sprintf("%s/binary-%s/%s", pfd.Properties.GetByName(debian_module.PropertyComponent), pfd.Properties.GetByName(debian_module.PropertyArchitecture), pfd.File.Name)
   354  		fmt.Fprintf(&md5, " %s %d %s\n", pfd.Blob.HashMD5, pfd.Blob.Size, path)
   355  		fmt.Fprintf(&sha1, " %s %d %s\n", pfd.Blob.HashSHA1, pfd.Blob.Size, path)
   356  		fmt.Fprintf(&sha256, " %s %d %s\n", pfd.Blob.HashSHA256, pfd.Blob.Size, path)
   357  		fmt.Fprintf(&sha512, " %s %d %s\n", pfd.Blob.HashSHA512, pfd.Blob.Size, path)
   358  	}
   359  
   360  	fmt.Fprintln(w, "MD5Sum:")
   361  	fmt.Fprint(w, md5.String())
   362  	fmt.Fprintln(w, "SHA1:")
   363  	fmt.Fprint(w, sha1.String())
   364  	fmt.Fprintln(w, "SHA256:")
   365  	fmt.Fprint(w, sha256.String())
   366  	fmt.Fprintln(w, "SHA512:")
   367  	fmt.Fprint(w, sha512.String())
   368  
   369  	sw.Close()
   370  
   371  	releaseGpgContent, _ := packages_module.NewHashedBuffer()
   372  	defer releaseGpgContent.Close()
   373  
   374  	if err := openpgp.ArmoredDetachSign(releaseGpgContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
   375  		return err
   376  	}
   377  
   378  	releaseContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
   379  	defer releaseContent.Close()
   380  
   381  	for _, file := range []struct {
   382  		Name string
   383  		Data packages_module.HashedSizeReader
   384  	}{
   385  		{"Release", releaseContent},
   386  		{"Release.gpg", releaseGpgContent},
   387  		{"InRelease", inReleaseContent},
   388  	} {
   389  		_, err = packages_service.AddFileToPackageVersionInternal(
   390  			ctx,
   391  			repoVersion,
   392  			&packages_service.PackageFileCreationInfo{
   393  				PackageFileInfo: packages_service.PackageFileInfo{
   394  					Filename:     file.Name,
   395  					CompositeKey: distribution,
   396  				},
   397  				Creator:           user_model.NewGhostUser(),
   398  				Data:              file.Data,
   399  				IsLead:            false,
   400  				OverwriteExisting: true,
   401  				Properties: map[string]string{
   402  					debian_module.PropertyDistribution: distribution,
   403  				},
   404  			},
   405  		)
   406  		if err != nil {
   407  			return err
   408  		}
   409  	}
   410  
   411  	return nil
   412  }