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