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 }