code.gitea.io/gitea@v1.22.3/services/packages/alpine/repository.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package alpine 5 6 import ( 7 "archive/tar" 8 "bytes" 9 "compress/gzip" 10 "context" 11 "crypto" 12 "crypto/rand" 13 "crypto/rsa" 14 "crypto/sha1" 15 "crypto/x509" 16 "encoding/hex" 17 "encoding/pem" 18 "errors" 19 "fmt" 20 "io" 21 "strings" 22 23 packages_model "code.gitea.io/gitea/models/packages" 24 alpine_model "code.gitea.io/gitea/models/packages/alpine" 25 user_model "code.gitea.io/gitea/models/user" 26 "code.gitea.io/gitea/modules/container" 27 "code.gitea.io/gitea/modules/json" 28 packages_module "code.gitea.io/gitea/modules/packages" 29 alpine_module "code.gitea.io/gitea/modules/packages/alpine" 30 "code.gitea.io/gitea/modules/util" 31 packages_service "code.gitea.io/gitea/services/packages" 32 ) 33 34 const ( 35 IndexFilename = "APKINDEX" 36 IndexArchiveFilename = IndexFilename + ".tar.gz" 37 ) 38 39 // GetOrCreateRepositoryVersion gets or creates the internal repository package 40 // The Alpine registry needs multiple index files which are stored in this package. 41 func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) { 42 return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion) 43 } 44 45 // GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files 46 func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) { 47 priv, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPrivate) 48 if err != nil && !errors.Is(err, util.ErrNotExist) { 49 return "", "", err 50 } 51 52 pub, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPublic) 53 if err != nil && !errors.Is(err, util.ErrNotExist) { 54 return "", "", err 55 } 56 57 if priv == "" || pub == "" { 58 priv, pub, err = util.GenerateKeyPair(4096) 59 if err != nil { 60 return "", "", err 61 } 62 63 if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPrivate, priv); err != nil { 64 return "", "", err 65 } 66 67 if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPublic, pub); err != nil { 68 return "", "", err 69 } 70 } 71 72 return priv, pub, nil 73 } 74 75 // BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures 76 func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { 77 pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) 78 if err != nil { 79 return err 80 } 81 82 // 1. Delete all existing repository files 83 pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) 84 if err != nil { 85 return err 86 } 87 88 for _, pf := range pfs { 89 if err := packages_service.DeletePackageFile(ctx, pf); err != nil { 90 return err 91 } 92 } 93 94 // 2. (Re)Build repository files for existing packages 95 branches, err := alpine_model.GetBranches(ctx, ownerID) 96 if err != nil { 97 return err 98 } 99 for _, branch := range branches { 100 repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch) 101 if err != nil { 102 return err 103 } 104 for _, repository := range repositories { 105 architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository) 106 if err != nil { 107 return err 108 } 109 for _, architecture := range architectures { 110 if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil { 111 return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err) 112 } 113 } 114 } 115 } 116 117 return nil 118 } 119 120 // BuildSpecificRepositoryFiles builds index files for the repository 121 func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error { 122 pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) 123 if err != nil { 124 return err 125 } 126 127 architectures := container.SetOf(architecture) 128 if architecture == alpine_module.NoArch { 129 // Update all other architectures too when updating the noarch index 130 additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository) 131 if err != nil { 132 return err 133 } 134 architectures.AddMultiple(additionalArchitectures...) 135 } 136 137 for architecture := range architectures { 138 if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil { 139 return err 140 } 141 } 142 return nil 143 } 144 145 type packageData struct { 146 Package *packages_model.Package 147 Version *packages_model.PackageVersion 148 Blob *packages_model.PackageBlob 149 VersionMetadata *alpine_module.VersionMetadata 150 FileMetadata *alpine_module.FileMetadata 151 } 152 153 type packageCache = map[*packages_model.PackageFile]*packageData 154 155 func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) { 156 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 157 OwnerID: ownerID, 158 PackageType: packages_model.TypeAlpine, 159 Query: "%.apk", 160 Properties: map[string]string{ 161 alpine_module.PropertyBranch: branch, 162 alpine_module.PropertyRepository: repository, 163 alpine_module.PropertyArchitecture: architecture, 164 }, 165 }) 166 if err != nil { 167 return nil, err 168 } 169 return pfs, nil 170 } 171 172 // https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format 173 func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error { 174 pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture) 175 if err != nil { 176 return err 177 } 178 if architecture != alpine_module.NoArch { 179 // Add all noarch packages too 180 noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch) 181 if err != nil { 182 return err 183 } 184 pfs = append(pfs, noarchFiles...) 185 } 186 187 // Delete the package indices if there are no packages 188 if len(pfs) == 0 { 189 pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture)) 190 if err != nil && !errors.Is(err, util.ErrNotExist) { 191 return err 192 } else if pf == nil { 193 return nil 194 } 195 196 return packages_service.DeletePackageFile(ctx, pf) 197 } 198 199 // Cache data needed for all repository files 200 cache := make(packageCache) 201 for _, pf := range pfs { 202 pv, err := packages_model.GetVersionByID(ctx, pf.VersionID) 203 if err != nil { 204 return err 205 } 206 p, err := packages_model.GetPackageByID(ctx, pv.PackageID) 207 if err != nil { 208 return err 209 } 210 pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) 211 if err != nil { 212 return err 213 } 214 pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata) 215 if err != nil { 216 return err 217 } 218 219 pd := &packageData{ 220 Package: p, 221 Version: pv, 222 Blob: pb, 223 } 224 225 if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil { 226 return err 227 } 228 if len(pps) > 0 { 229 if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil { 230 return err 231 } 232 } 233 234 cache[pf] = pd 235 } 236 237 var buf bytes.Buffer 238 for _, pf := range pfs { 239 pd := cache[pf] 240 241 fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum) 242 fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name) 243 fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version) 244 fmt.Fprintf(&buf, "A:%s\n", architecture) 245 if pd.VersionMetadata.Description != "" { 246 fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description) 247 } 248 if pd.VersionMetadata.ProjectURL != "" { 249 fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL) 250 } 251 if pd.VersionMetadata.License != "" { 252 fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License) 253 } 254 fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size) 255 fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size) 256 fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin) 257 fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer) 258 fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate) 259 if pd.FileMetadata.CommitHash != "" { 260 fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash) 261 } 262 if len(pd.FileMetadata.Dependencies) > 0 { 263 fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " ")) 264 } 265 if len(pd.FileMetadata.Provides) > 0 { 266 fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " ")) 267 } 268 if pd.FileMetadata.InstallIf != "" { 269 fmt.Fprintf(&buf, "i:%s\n", pd.FileMetadata.InstallIf) 270 } 271 if pd.FileMetadata.ProviderPriority > 0 { 272 fmt.Fprintf(&buf, "k:%d\n", pd.FileMetadata.ProviderPriority) 273 } 274 fmt.Fprint(&buf, "\n") 275 } 276 277 unsignedIndexContent, _ := packages_module.NewHashedBuffer() 278 defer unsignedIndexContent.Close() 279 280 h := sha1.New() 281 282 if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil { 283 return err 284 } 285 286 priv, _, err := GetOrCreateKeyPair(ctx, ownerID) 287 if err != nil { 288 return err 289 } 290 291 privPem, _ := pem.Decode([]byte(priv)) 292 if privPem == nil { 293 return fmt.Errorf("failed to decode private key pem") 294 } 295 296 privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) 297 if err != nil { 298 return err 299 } 300 301 sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil)) 302 if err != nil { 303 return err 304 } 305 306 owner, err := user_model.GetUserByID(ctx, ownerID) 307 if err != nil { 308 return err 309 } 310 311 fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey) 312 if err != nil { 313 return err 314 } 315 316 signedIndexContent, _ := packages_module.NewHashedBuffer() 317 defer signedIndexContent.Close() 318 319 if err := writeGzipStream( 320 signedIndexContent, 321 fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)), 322 sign, 323 false, 324 ); err != nil { 325 return err 326 } 327 328 if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil { 329 return err 330 } 331 332 _, err = packages_service.AddFileToPackageVersionInternal( 333 ctx, 334 repoVersion, 335 &packages_service.PackageFileCreationInfo{ 336 PackageFileInfo: packages_service.PackageFileInfo{ 337 Filename: IndexArchiveFilename, 338 CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), 339 }, 340 Creator: user_model.NewGhostUser(), 341 Data: signedIndexContent, 342 IsLead: false, 343 OverwriteExisting: true, 344 Properties: map[string]string{ 345 alpine_module.PropertyBranch: branch, 346 alpine_module.PropertyRepository: repository, 347 alpine_module.PropertyArchitecture: architecture, 348 }, 349 }, 350 ) 351 return err 352 } 353 354 func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error { 355 zw := gzip.NewWriter(w) 356 defer zw.Close() 357 358 tw := tar.NewWriter(zw) 359 if addTarEnd { 360 defer tw.Close() 361 } 362 hdr := &tar.Header{ 363 Name: filename, 364 Mode: 0o600, 365 Size: int64(len(content)), 366 } 367 if err := tw.WriteHeader(hdr); err != nil { 368 return err 369 } 370 if _, err := tw.Write(content); err != nil { 371 return err 372 } 373 return nil 374 }