code.gitea.io/gitea@v1.22.3/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  	opts := &debian_model.PackageSearchOptions{
   166  		OwnerID:      ownerID,
   167  		Distribution: distribution,
   168  		Component:    component,
   169  		Architecture: architecture,
   170  	}
   171  
   172  	// Delete the package indices if there are no packages
   173  	if has, err := debian_model.ExistPackages(ctx, opts); err != nil {
   174  		return err
   175  	} else if !has {
   176  		key := fmt.Sprintf("%s|%s|%s", distribution, component, architecture)
   177  		for _, filename := range []string{"Packages", "Packages.gz", "Packages.xz"} {
   178  			pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
   179  			if err != nil && !errors.Is(err, util.ErrNotExist) {
   180  				return err
   181  			} else if pf == nil {
   182  				continue
   183  			}
   184  
   185  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   186  				return err
   187  			}
   188  		}
   189  
   190  		return nil
   191  	}
   192  
   193  	packagesContent, _ := packages_module.NewHashedBuffer()
   194  	defer packagesContent.Close()
   195  
   196  	packagesGzipContent, _ := packages_module.NewHashedBuffer()
   197  	defer packagesGzipContent.Close()
   198  
   199  	gzw := gzip.NewWriter(packagesGzipContent)
   200  
   201  	packagesXzContent, _ := packages_module.NewHashedBuffer()
   202  	defer packagesXzContent.Close()
   203  
   204  	xzw, _ := xz.NewWriter(packagesXzContent)
   205  
   206  	w := io.MultiWriter(packagesContent, gzw, xzw)
   207  
   208  	addSeparator := false
   209  	if err := debian_model.SearchPackages(ctx, opts, func(pfd *packages_model.PackageFileDescriptor) {
   210  		if addSeparator {
   211  			fmt.Fprintln(w)
   212  		}
   213  		addSeparator = true
   214  
   215  		fmt.Fprintf(w, "%s\n", strings.TrimSpace(pfd.Properties.GetByName(debian_module.PropertyControl)))
   216  
   217  		fmt.Fprintf(w, "Filename: pool/%s/%s/%s\n", distribution, component, pfd.File.Name)
   218  		fmt.Fprintf(w, "Size: %d\n", pfd.Blob.Size)
   219  		fmt.Fprintf(w, "MD5sum: %s\n", pfd.Blob.HashMD5)
   220  		fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1)
   221  		fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256)
   222  		fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512)
   223  	}); err != nil {
   224  		return err
   225  	}
   226  
   227  	gzw.Close()
   228  	xzw.Close()
   229  
   230  	for _, file := range []struct {
   231  		Name string
   232  		Data packages_module.HashedSizeReader
   233  	}{
   234  		{"Packages", packagesContent},
   235  		{"Packages.gz", packagesGzipContent},
   236  		{"Packages.xz", packagesXzContent},
   237  	} {
   238  		_, err := packages_service.AddFileToPackageVersionInternal(
   239  			ctx,
   240  			repoVersion,
   241  			&packages_service.PackageFileCreationInfo{
   242  				PackageFileInfo: packages_service.PackageFileInfo{
   243  					Filename:     file.Name,
   244  					CompositeKey: fmt.Sprintf("%s|%s|%s", distribution, component, architecture),
   245  				},
   246  				Creator:           user_model.NewGhostUser(),
   247  				Data:              file.Data,
   248  				IsLead:            false,
   249  				OverwriteExisting: true,
   250  				Properties: map[string]string{
   251  					debian_module.PropertyRepositoryIncludeInRelease: "",
   252  					debian_module.PropertyDistribution:               distribution,
   253  					debian_module.PropertyComponent:                  component,
   254  					debian_module.PropertyArchitecture:               architecture,
   255  				},
   256  			},
   257  		)
   258  		if err != nil {
   259  			return err
   260  		}
   261  	}
   262  
   263  	return nil
   264  }
   265  
   266  // https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
   267  func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution string) error {
   268  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   269  		VersionID: repoVersion.ID,
   270  		Properties: map[string]string{
   271  			debian_module.PropertyRepositoryIncludeInRelease: "",
   272  			debian_module.PropertyDistribution:               distribution,
   273  		},
   274  	})
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	// Delete the release files if there are no packages
   280  	if len(pfs) == 0 {
   281  		for _, filename := range []string{"Release", "Release.gpg", "InRelease"} {
   282  			pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
   283  			if err != nil && !errors.Is(err, util.ErrNotExist) {
   284  				return err
   285  			} else if pf == nil {
   286  				continue
   287  			}
   288  
   289  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   290  				return err
   291  			}
   292  		}
   293  
   294  		return nil
   295  	}
   296  
   297  	components, err := debian_model.GetComponents(ctx, ownerID, distribution)
   298  	if err != nil {
   299  		return err
   300  	}
   301  
   302  	sort.Strings(components)
   303  
   304  	architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	sort.Strings(architectures)
   310  
   311  	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	block, err := armor.Decode(strings.NewReader(priv))
   317  	if err != nil {
   318  		return err
   319  	}
   320  
   321  	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
   322  	if err != nil {
   323  		return err
   324  	}
   325  
   326  	inReleaseContent, _ := packages_module.NewHashedBuffer()
   327  	defer inReleaseContent.Close()
   328  
   329  	sw, err := clearsign.Encode(inReleaseContent, e.PrivateKey, nil)
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	var buf bytes.Buffer
   335  
   336  	w := io.MultiWriter(sw, &buf)
   337  
   338  	fmt.Fprintf(w, "Origin: %s\n", setting.AppName)
   339  	fmt.Fprintf(w, "Label: %s\n", setting.AppName)
   340  	fmt.Fprintf(w, "Suite: %s\n", distribution)
   341  	fmt.Fprintf(w, "Codename: %s\n", distribution)
   342  	fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
   343  	fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
   344  	fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
   345  	fmt.Fprint(w, "Acquire-By-Hash: yes\n")
   346  
   347  	pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	var md5, sha1, sha256, sha512 strings.Builder
   353  	for _, pfd := range pfds {
   354  		path := fmt.Sprintf("%s/binary-%s/%s", pfd.Properties.GetByName(debian_module.PropertyComponent), pfd.Properties.GetByName(debian_module.PropertyArchitecture), pfd.File.Name)
   355  		fmt.Fprintf(&md5, " %s %d %s\n", pfd.Blob.HashMD5, pfd.Blob.Size, path)
   356  		fmt.Fprintf(&sha1, " %s %d %s\n", pfd.Blob.HashSHA1, pfd.Blob.Size, path)
   357  		fmt.Fprintf(&sha256, " %s %d %s\n", pfd.Blob.HashSHA256, pfd.Blob.Size, path)
   358  		fmt.Fprintf(&sha512, " %s %d %s\n", pfd.Blob.HashSHA512, pfd.Blob.Size, path)
   359  	}
   360  
   361  	fmt.Fprintln(w, "MD5Sum:")
   362  	fmt.Fprint(w, md5.String())
   363  	fmt.Fprintln(w, "SHA1:")
   364  	fmt.Fprint(w, sha1.String())
   365  	fmt.Fprintln(w, "SHA256:")
   366  	fmt.Fprint(w, sha256.String())
   367  	fmt.Fprintln(w, "SHA512:")
   368  	fmt.Fprint(w, sha512.String())
   369  
   370  	sw.Close()
   371  
   372  	releaseGpgContent, _ := packages_module.NewHashedBuffer()
   373  	defer releaseGpgContent.Close()
   374  
   375  	if err := openpgp.ArmoredDetachSign(releaseGpgContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
   376  		return err
   377  	}
   378  
   379  	releaseContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
   380  	defer releaseContent.Close()
   381  
   382  	for _, file := range []struct {
   383  		Name string
   384  		Data packages_module.HashedSizeReader
   385  	}{
   386  		{"Release", releaseContent},
   387  		{"Release.gpg", releaseGpgContent},
   388  		{"InRelease", inReleaseContent},
   389  	} {
   390  		_, err = packages_service.AddFileToPackageVersionInternal(
   391  			ctx,
   392  			repoVersion,
   393  			&packages_service.PackageFileCreationInfo{
   394  				PackageFileInfo: packages_service.PackageFileInfo{
   395  					Filename:     file.Name,
   396  					CompositeKey: distribution,
   397  				},
   398  				Creator:           user_model.NewGhostUser(),
   399  				Data:              file.Data,
   400  				IsLead:            false,
   401  				OverwriteExisting: true,
   402  				Properties: map[string]string{
   403  					debian_module.PropertyDistribution: distribution,
   404  				},
   405  			},
   406  		)
   407  		if err != nil {
   408  			return err
   409  		}
   410  	}
   411  
   412  	return nil
   413  }