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 }