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