code.gitea.io/gitea@v1.22.3/services/packages/rpm/repository.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package rpm 5 6 import ( 7 "bytes" 8 "compress/gzip" 9 "context" 10 "crypto/sha256" 11 "encoding/hex" 12 "encoding/xml" 13 "errors" 14 "fmt" 15 "io" 16 "strings" 17 "time" 18 19 packages_model "code.gitea.io/gitea/models/packages" 20 rpm_model "code.gitea.io/gitea/models/packages/rpm" 21 user_model "code.gitea.io/gitea/models/user" 22 "code.gitea.io/gitea/modules/json" 23 packages_module "code.gitea.io/gitea/modules/packages" 24 rpm_module "code.gitea.io/gitea/modules/packages/rpm" 25 "code.gitea.io/gitea/modules/util" 26 packages_service "code.gitea.io/gitea/services/packages" 27 28 "github.com/keybase/go-crypto/openpgp" 29 "github.com/keybase/go-crypto/openpgp/armor" 30 "github.com/keybase/go-crypto/openpgp/packet" 31 ) 32 33 // GetOrCreateRepositoryVersion gets or creates the internal repository package 34 // The RPM registry needs multiple metadata 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.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion) 37 } 38 39 // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files 40 func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) { 41 priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate) 42 if err != nil && !errors.Is(err, util.ErrNotExist) { 43 return "", "", err 44 } 45 46 pub, err := user_model.GetSetting(ctx, ownerID, rpm_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, rpm_module.SettingKeyPrivate, priv); err != nil { 58 return "", "", err 59 } 60 61 if err := user_model.SetUserSetting(ctx, ownerID, rpm_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("", "RPM 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 group 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 groups, err := rpm_model.GetGroups(ctx, ownerID) 120 if err != nil { 121 return err 122 } 123 for _, group := range groups { 124 if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil { 125 return fmt.Errorf("failed to build repository files [%s]: %w", group, err) 126 } 127 } 128 129 return nil 130 } 131 132 type repoChecksum struct { 133 Value string `xml:",chardata"` 134 Type string `xml:"type,attr"` 135 } 136 137 type repoLocation struct { 138 Href string `xml:"href,attr"` 139 } 140 141 type repoData struct { 142 Type string `xml:"type,attr"` 143 Checksum repoChecksum `xml:"checksum"` 144 OpenChecksum repoChecksum `xml:"open-checksum"` 145 Location repoLocation `xml:"location"` 146 Timestamp int64 `xml:"timestamp"` 147 Size int64 `xml:"size"` 148 OpenSize int64 `xml:"open-size"` 149 } 150 151 type packageData struct { 152 Package *packages_model.Package 153 Version *packages_model.PackageVersion 154 Blob *packages_model.PackageBlob 155 VersionMetadata *rpm_module.VersionMetadata 156 FileMetadata *rpm_module.FileMetadata 157 } 158 159 type packageCache = map[*packages_model.PackageFile]*packageData 160 161 // BuildSpecificRepositoryFiles builds metadata files for the repository 162 func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group string) error { 163 pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) 164 if err != nil { 165 return err 166 } 167 168 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 169 OwnerID: ownerID, 170 PackageType: packages_model.TypeRpm, 171 Query: "%.rpm", 172 CompositeKey: group, 173 }) 174 if err != nil { 175 return err 176 } 177 178 // Delete the repository files if there are no packages 179 if len(pfs) == 0 { 180 pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) 181 if err != nil { 182 return err 183 } 184 for _, pf := range pfs { 185 if err := packages_service.DeletePackageFile(ctx, pf); err != nil { 186 return err 187 } 188 } 189 190 return nil 191 } 192 193 // Cache data needed for all repository files 194 cache := make(packageCache) 195 for _, pf := range pfs { 196 pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) 197 if err != nil { 198 return err 199 } 200 p, err := packages_model.GetPackageByID(ctx, pv.PackageID) 201 if err != nil { 202 return err 203 } 204 pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) 205 if err != nil { 206 return err 207 } 208 pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata) 209 if err != nil { 210 return err 211 } 212 213 pd := &packageData{ 214 Package: p, 215 Version: pv, 216 Blob: pb, 217 } 218 219 if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil { 220 return err 221 } 222 if len(pps) > 0 { 223 if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil { 224 return err 225 } 226 } 227 228 cache[pf] = pd 229 } 230 231 primary, err := buildPrimary(ctx, pv, pfs, cache, group) 232 if err != nil { 233 return err 234 } 235 filelists, err := buildFilelists(ctx, pv, pfs, cache, group) 236 if err != nil { 237 return err 238 } 239 other, err := buildOther(ctx, pv, pfs, cache, group) 240 if err != nil { 241 return err 242 } 243 244 return buildRepomd( 245 ctx, 246 pv, 247 ownerID, 248 []*repoData{ 249 primary, 250 filelists, 251 other, 252 }, 253 group, 254 ) 255 } 256 257 // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml 258 func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, group string) error { 259 type Repomd struct { 260 XMLName xml.Name `xml:"repomd"` 261 Xmlns string `xml:"xmlns,attr"` 262 XmlnsRpm string `xml:"xmlns:rpm,attr"` 263 Data []*repoData `xml:"data"` 264 } 265 266 var buf bytes.Buffer 267 buf.WriteString(xml.Header) 268 if err := xml.NewEncoder(&buf).Encode(&Repomd{ 269 Xmlns: "http://linux.duke.edu/metadata/repo", 270 XmlnsRpm: "http://linux.duke.edu/metadata/rpm", 271 Data: data, 272 }); err != nil { 273 return err 274 } 275 276 priv, _, err := GetOrCreateKeyPair(ctx, ownerID) 277 if err != nil { 278 return err 279 } 280 281 block, err := armor.Decode(strings.NewReader(priv)) 282 if err != nil { 283 return err 284 } 285 286 e, err := openpgp.ReadEntity(packet.NewReader(block.Body)) 287 if err != nil { 288 return err 289 } 290 291 repomdAscContent, _ := packages_module.NewHashedBuffer() 292 defer repomdAscContent.Close() 293 294 if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil { 295 return err 296 } 297 298 repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf) 299 defer repomdContent.Close() 300 301 for _, file := range []struct { 302 Name string 303 Data packages_module.HashedSizeReader 304 }{ 305 {"repomd.xml", repomdContent}, 306 {"repomd.xml.asc", repomdAscContent}, 307 } { 308 _, err = packages_service.AddFileToPackageVersionInternal( 309 ctx, 310 pv, 311 &packages_service.PackageFileCreationInfo{ 312 PackageFileInfo: packages_service.PackageFileInfo{ 313 Filename: file.Name, 314 CompositeKey: group, 315 }, 316 Creator: user_model.NewGhostUser(), 317 Data: file.Data, 318 IsLead: false, 319 OverwriteExisting: true, 320 }, 321 ) 322 if err != nil { 323 return err 324 } 325 } 326 327 return nil 328 } 329 330 // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml 331 func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { 332 type Version struct { 333 Epoch string `xml:"epoch,attr"` 334 Version string `xml:"ver,attr"` 335 Release string `xml:"rel,attr"` 336 } 337 338 type Checksum struct { 339 Checksum string `xml:",chardata"` 340 Type string `xml:"type,attr"` 341 Pkgid string `xml:"pkgid,attr"` 342 } 343 344 type Times struct { 345 File uint64 `xml:"file,attr"` 346 Build uint64 `xml:"build,attr"` 347 } 348 349 type Sizes struct { 350 Package int64 `xml:"package,attr"` 351 Installed uint64 `xml:"installed,attr"` 352 Archive uint64 `xml:"archive,attr"` 353 } 354 355 type Location struct { 356 Href string `xml:"href,attr"` 357 } 358 359 type EntryList struct { 360 Entries []*rpm_module.Entry `xml:"rpm:entry"` 361 } 362 363 type Format struct { 364 License string `xml:"rpm:license"` 365 Vendor string `xml:"rpm:vendor"` 366 Group string `xml:"rpm:group"` 367 Buildhost string `xml:"rpm:buildhost"` 368 Sourcerpm string `xml:"rpm:sourcerpm"` 369 Provides EntryList `xml:"rpm:provides"` 370 Requires EntryList `xml:"rpm:requires"` 371 Conflicts EntryList `xml:"rpm:conflicts"` 372 Obsoletes EntryList `xml:"rpm:obsoletes"` 373 Files []*rpm_module.File `xml:"file"` 374 } 375 376 type Package struct { 377 XMLName xml.Name `xml:"package"` 378 Type string `xml:"type,attr"` 379 Name string `xml:"name"` 380 Architecture string `xml:"arch"` 381 Version Version `xml:"version"` 382 Checksum Checksum `xml:"checksum"` 383 Summary string `xml:"summary"` 384 Description string `xml:"description"` 385 Packager string `xml:"packager"` 386 URL string `xml:"url"` 387 Time Times `xml:"time"` 388 Size Sizes `xml:"size"` 389 Location Location `xml:"location"` 390 Format Format `xml:"format"` 391 } 392 393 type Metadata struct { 394 XMLName xml.Name `xml:"metadata"` 395 Xmlns string `xml:"xmlns,attr"` 396 XmlnsRpm string `xml:"xmlns:rpm,attr"` 397 PackageCount int `xml:"packages,attr"` 398 Packages []*Package `xml:"package"` 399 } 400 401 packages := make([]*Package, 0, len(pfs)) 402 for _, pf := range pfs { 403 pd := c[pf] 404 405 files := make([]*rpm_module.File, 0, 3) 406 for _, f := range pd.FileMetadata.Files { 407 if f.IsExecutable { 408 files = append(files, f) 409 } 410 } 411 packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release) 412 packages = append(packages, &Package{ 413 Type: "rpm", 414 Name: pd.Package.Name, 415 Architecture: pd.FileMetadata.Architecture, 416 Version: Version{ 417 Epoch: pd.FileMetadata.Epoch, 418 Version: pd.FileMetadata.Version, 419 Release: pd.FileMetadata.Release, 420 }, 421 Checksum: Checksum{ 422 Type: "sha256", 423 Checksum: pd.Blob.HashSHA256, 424 Pkgid: "YES", 425 }, 426 Summary: pd.VersionMetadata.Summary, 427 Description: pd.VersionMetadata.Description, 428 Packager: pd.FileMetadata.Packager, 429 URL: pd.VersionMetadata.ProjectURL, 430 Time: Times{ 431 File: pd.FileMetadata.FileTime, 432 Build: pd.FileMetadata.BuildTime, 433 }, 434 Size: Sizes{ 435 Package: pd.Blob.Size, 436 Installed: pd.FileMetadata.InstalledSize, 437 Archive: pd.FileMetadata.ArchiveSize, 438 }, 439 Location: Location{ 440 Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture), 441 }, 442 Format: Format{ 443 License: pd.VersionMetadata.License, 444 Vendor: pd.FileMetadata.Vendor, 445 Group: pd.FileMetadata.Group, 446 Buildhost: pd.FileMetadata.BuildHost, 447 Sourcerpm: pd.FileMetadata.SourceRpm, 448 Provides: EntryList{ 449 Entries: pd.FileMetadata.Provides, 450 }, 451 Requires: EntryList{ 452 Entries: pd.FileMetadata.Requires, 453 }, 454 Conflicts: EntryList{ 455 Entries: pd.FileMetadata.Conflicts, 456 }, 457 Obsoletes: EntryList{ 458 Entries: pd.FileMetadata.Obsoletes, 459 }, 460 Files: files, 461 }, 462 }) 463 } 464 465 return addDataAsFileToRepo(ctx, pv, "primary", &Metadata{ 466 Xmlns: "http://linux.duke.edu/metadata/common", 467 XmlnsRpm: "http://linux.duke.edu/metadata/rpm", 468 PackageCount: len(pfs), 469 Packages: packages, 470 }, group) 471 } 472 473 // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml 474 func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl 475 type Version struct { 476 Epoch string `xml:"epoch,attr"` 477 Version string `xml:"ver,attr"` 478 Release string `xml:"rel,attr"` 479 } 480 481 type Package struct { 482 Pkgid string `xml:"pkgid,attr"` 483 Name string `xml:"name,attr"` 484 Architecture string `xml:"arch,attr"` 485 Version Version `xml:"version"` 486 Files []*rpm_module.File `xml:"file"` 487 } 488 489 type Filelists struct { 490 XMLName xml.Name `xml:"filelists"` 491 Xmlns string `xml:"xmlns,attr"` 492 PackageCount int `xml:"packages,attr"` 493 Packages []*Package `xml:"package"` 494 } 495 496 packages := make([]*Package, 0, len(pfs)) 497 for _, pf := range pfs { 498 pd := c[pf] 499 500 packages = append(packages, &Package{ 501 Pkgid: pd.Blob.HashSHA256, 502 Name: pd.Package.Name, 503 Architecture: pd.FileMetadata.Architecture, 504 Version: Version{ 505 Epoch: pd.FileMetadata.Epoch, 506 Version: pd.FileMetadata.Version, 507 Release: pd.FileMetadata.Release, 508 }, 509 Files: pd.FileMetadata.Files, 510 }) 511 } 512 513 return addDataAsFileToRepo(ctx, pv, "filelists", &Filelists{ 514 Xmlns: "http://linux.duke.edu/metadata/other", 515 PackageCount: len(pfs), 516 Packages: packages, 517 }, group) 518 } 519 520 // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml 521 func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl 522 type Version struct { 523 Epoch string `xml:"epoch,attr"` 524 Version string `xml:"ver,attr"` 525 Release string `xml:"rel,attr"` 526 } 527 528 type Package struct { 529 Pkgid string `xml:"pkgid,attr"` 530 Name string `xml:"name,attr"` 531 Architecture string `xml:"arch,attr"` 532 Version Version `xml:"version"` 533 Changelogs []*rpm_module.Changelog `xml:"changelog"` 534 } 535 536 type Otherdata struct { 537 XMLName xml.Name `xml:"otherdata"` 538 Xmlns string `xml:"xmlns,attr"` 539 PackageCount int `xml:"packages,attr"` 540 Packages []*Package `xml:"package"` 541 } 542 543 packages := make([]*Package, 0, len(pfs)) 544 for _, pf := range pfs { 545 pd := c[pf] 546 547 packages = append(packages, &Package{ 548 Pkgid: pd.Blob.HashSHA256, 549 Name: pd.Package.Name, 550 Architecture: pd.FileMetadata.Architecture, 551 Version: Version{ 552 Epoch: pd.FileMetadata.Epoch, 553 Version: pd.FileMetadata.Version, 554 Release: pd.FileMetadata.Release, 555 }, 556 Changelogs: pd.FileMetadata.Changelogs, 557 }) 558 } 559 560 return addDataAsFileToRepo(ctx, pv, "other", &Otherdata{ 561 Xmlns: "http://linux.duke.edu/metadata/other", 562 PackageCount: len(pfs), 563 Packages: packages, 564 }, group) 565 } 566 567 // writtenCounter counts all written bytes 568 type writtenCounter struct { 569 written int64 570 } 571 572 func (wc *writtenCounter) Write(buf []byte) (int, error) { 573 n := len(buf) 574 575 wc.written += int64(n) 576 577 return n, nil 578 } 579 580 func (wc *writtenCounter) Written() int64 { 581 return wc.written 582 } 583 584 func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) { 585 content, _ := packages_module.NewHashedBuffer() 586 defer content.Close() 587 588 gzw := gzip.NewWriter(content) 589 wc := &writtenCounter{} 590 h := sha256.New() 591 592 w := io.MultiWriter(gzw, wc, h) 593 _, _ = w.Write([]byte(xml.Header)) 594 595 if err := xml.NewEncoder(w).Encode(obj); err != nil { 596 return nil, err 597 } 598 599 if err := gzw.Close(); err != nil { 600 return nil, err 601 } 602 603 filename := filetype + ".xml.gz" 604 605 _, err := packages_service.AddFileToPackageVersionInternal( 606 ctx, 607 pv, 608 &packages_service.PackageFileCreationInfo{ 609 PackageFileInfo: packages_service.PackageFileInfo{ 610 Filename: filename, 611 CompositeKey: group, 612 }, 613 Creator: user_model.NewGhostUser(), 614 Data: content, 615 IsLead: false, 616 OverwriteExisting: true, 617 }, 618 ) 619 if err != nil { 620 return nil, err 621 } 622 623 _, _, hashSHA256, _ := content.Sums() 624 625 return &repoData{ 626 Type: filetype, 627 Checksum: repoChecksum{ 628 Type: "sha256", 629 Value: hex.EncodeToString(hashSHA256), 630 }, 631 OpenChecksum: repoChecksum{ 632 Type: "sha256", 633 Value: hex.EncodeToString(h.Sum(nil)), 634 }, 635 Location: repoLocation{ 636 Href: "repodata/" + filename, 637 }, 638 Timestamp: time.Now().Unix(), 639 Size: content.Size(), 640 OpenSize: wc.Written(), 641 }, nil 642 }