github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/image/helpers.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package image 21 22 // TODO: put these in appropriate package(s) once they are clarified a bit more 23 24 import ( 25 "bytes" 26 "context" 27 "crypto" 28 "encoding/json" 29 "errors" 30 "fmt" 31 "io/ioutil" 32 "net/url" 33 "os" 34 "os/signal" 35 "path/filepath" 36 "strings" 37 "syscall" 38 39 "github.com/mvo5/goconfigparser" 40 41 "github.com/snapcore/snapd/asserts" 42 "github.com/snapcore/snapd/asserts/snapasserts" 43 "github.com/snapcore/snapd/logger" 44 "github.com/snapcore/snapd/osutil" 45 "github.com/snapcore/snapd/overlord/auth" 46 "github.com/snapcore/snapd/progress" 47 "github.com/snapcore/snapd/release" 48 "github.com/snapcore/snapd/snap" 49 "github.com/snapcore/snapd/snapdenv" 50 "github.com/snapcore/snapd/store" 51 "github.com/snapcore/snapd/strutil" 52 ) 53 54 // A Store can find metadata on snaps, download snaps and fetch assertions. 55 type Store interface { 56 SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, store.AssertionQuery, *auth.UserState, *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) 57 Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error 58 59 Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) 60 } 61 62 // ToolingStore wraps access to the store for tools. 63 type ToolingStore struct { 64 sto Store 65 user *auth.UserState 66 } 67 68 func newToolingStore(arch, storeID string) (*ToolingStore, error) { 69 cfg := store.DefaultConfig() 70 cfg.Architecture = arch 71 cfg.StoreID = storeID 72 var user *auth.UserState 73 if authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME"); authFn != "" { 74 var err error 75 user, err = readAuthFile(authFn) 76 if err != nil { 77 return nil, err 78 } 79 } 80 sto := store.New(cfg, toolingStoreContext{}) 81 return &ToolingStore{ 82 sto: sto, 83 user: user, 84 }, nil 85 } 86 87 type authData struct { 88 Macaroon string `json:"macaroon"` 89 Discharges []string `json:"discharges"` 90 } 91 92 func readAuthFile(authFn string) (*auth.UserState, error) { 93 data, err := ioutil.ReadFile(authFn) 94 if err != nil { 95 return nil, fmt.Errorf("cannot read auth file %q: %v", authFn, err) 96 } 97 98 creds, err := parseAuthFile(authFn, data) 99 if err != nil { 100 // try snapcraft login format instead 101 var err2 error 102 creds, err2 = parseSnapcraftLoginFile(authFn, data) 103 if err2 != nil { 104 trimmed := bytes.TrimSpace(data) 105 if len(trimmed) > 0 && trimmed[0] == '[' { 106 return nil, err2 107 } 108 return nil, err 109 } 110 } 111 112 return &auth.UserState{ 113 StoreMacaroon: creds.Macaroon, 114 StoreDischarges: creds.Discharges, 115 }, nil 116 } 117 118 func parseAuthFile(authFn string, data []byte) (*authData, error) { 119 var creds authData 120 err := json.Unmarshal(data, &creds) 121 if err != nil { 122 return nil, fmt.Errorf("cannot decode auth file %q: %v", authFn, err) 123 } 124 if creds.Macaroon == "" || len(creds.Discharges) == 0 { 125 return nil, fmt.Errorf("invalid auth file %q: missing fields", authFn) 126 } 127 return &creds, nil 128 } 129 130 func snapcraftLoginSection() string { 131 if snapdenv.UseStagingStore() { 132 return "login.staging.ubuntu.com" 133 } 134 return "login.ubuntu.com" 135 } 136 137 func parseSnapcraftLoginFile(authFn string, data []byte) (*authData, error) { 138 errPrefix := fmt.Sprintf("invalid snapcraft login file %q", authFn) 139 140 cfg := goconfigparser.New() 141 if err := cfg.ReadString(string(data)); err != nil { 142 return nil, fmt.Errorf("%s: %v", errPrefix, err) 143 } 144 sec := snapcraftLoginSection() 145 macaroon, err := cfg.Get(sec, "macaroon") 146 if err != nil { 147 return nil, fmt.Errorf("%s: %s", errPrefix, err) 148 } 149 unboundDischarge, err := cfg.Get(sec, "unbound_discharge") 150 if err != nil { 151 return nil, fmt.Errorf("%s: %v", errPrefix, err) 152 } 153 if macaroon == "" || unboundDischarge == "" { 154 return nil, fmt.Errorf("invalid snapcraft login file %q: empty fields", authFn) 155 } 156 return &authData{ 157 Macaroon: macaroon, 158 Discharges: []string{unboundDischarge}, 159 }, nil 160 } 161 162 // toolingStoreContext implements trivially store.DeviceAndAuthContext 163 // except implementing UpdateUserAuth properly to be used to refresh a 164 // soft-expired user macaroon. 165 type toolingStoreContext struct{} 166 167 func (tac toolingStoreContext) CloudInfo() (*auth.CloudInfo, error) { 168 return nil, nil 169 } 170 171 func (tac toolingStoreContext) Device() (*auth.DeviceState, error) { 172 return &auth.DeviceState{}, nil 173 } 174 175 func (tac toolingStoreContext) DeviceSessionRequestParams(_ string) (*store.DeviceSessionRequestParams, error) { 176 return nil, store.ErrNoSerial 177 } 178 179 func (tac toolingStoreContext) ProxyStoreParams(defaultURL *url.URL) (proxyStoreID string, proxySroreURL *url.URL, err error) { 180 return "", defaultURL, nil 181 } 182 183 func (tac toolingStoreContext) StoreID(fallback string) (string, error) { 184 return fallback, nil 185 } 186 187 func (tac toolingStoreContext) UpdateDeviceAuth(_ *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) { 188 return nil, fmt.Errorf("internal error: no device state in tools") 189 } 190 191 func (tac toolingStoreContext) UpdateUserAuth(user *auth.UserState, discharges []string) (*auth.UserState, error) { 192 user.StoreDischarges = discharges 193 return user, nil 194 } 195 196 func NewToolingStoreFromModel(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error) { 197 architecture := model.Architecture() 198 // can happen on classic 199 if architecture == "" { 200 architecture = fallbackArchitecture 201 } 202 return newToolingStore(architecture, model.Store()) 203 } 204 205 func NewToolingStore() (*ToolingStore, error) { 206 arch := os.Getenv("UBUNTU_STORE_ARCH") 207 storeID := os.Getenv("UBUNTU_STORE_ID") 208 return newToolingStore(arch, storeID) 209 } 210 211 // DownloadOptions carries options for downloading snaps plus assertions. 212 type DownloadOptions struct { 213 TargetDir string 214 // if TargetPathFunc is not nil it will be invoked 215 // to compute the target path for the download and TargetDir is 216 // ignored 217 TargetPathFunc func(*snap.Info) (string, error) 218 219 Revision snap.Revision 220 Channel string 221 CohortKey string 222 Basename string 223 224 LeavePartialOnError bool 225 } 226 227 var ( 228 errRevisionAndCohort = errors.New("cannot specify both revision and cohort") 229 errPathInBase = errors.New("cannot specify a path in basename (use target dir for that)") 230 ) 231 232 func (opts *DownloadOptions) validate() error { 233 if strings.ContainsRune(opts.Basename, filepath.Separator) { 234 return errPathInBase 235 } 236 if !(opts.Revision.Unset() || opts.CohortKey == "") { 237 return errRevisionAndCohort 238 } 239 return nil 240 } 241 242 func (opts *DownloadOptions) String() string { 243 spec := make([]string, 0, 5) 244 if !opts.Revision.Unset() { 245 spec = append(spec, fmt.Sprintf("(%s)", opts.Revision)) 246 } 247 if opts.Channel != "" { 248 spec = append(spec, fmt.Sprintf("from channel %q", opts.Channel)) 249 } 250 if opts.CohortKey != "" { 251 // cohort keys are really long, and the rightmost bit being the 252 // interesting bit, so ellipt the rest 253 spec = append(spec, fmt.Sprintf(`from cohort %q`, strutil.ElliptLeft(opts.CohortKey, 10))) 254 } 255 if opts.Basename != "" { 256 spec = append(spec, fmt.Sprintf("to %q", opts.Basename+".snap")) 257 } 258 if opts.TargetDir != "" { 259 spec = append(spec, fmt.Sprintf("in %q", opts.TargetDir)) 260 } 261 return strings.Join(spec, " ") 262 } 263 264 // DownloadSnap downloads the snap with the given name and optionally 265 // revision using the provided store and options. It returns the final 266 // full path of the snap and a snap.Info for it and optionally a 267 // channel the snap got redirected to. 268 func (tsto *ToolingStore) DownloadSnap(name string, opts DownloadOptions) (targetFn string, info *snap.Info, redirectChannel string, err error) { 269 if err := opts.validate(); err != nil { 270 return "", nil, "", err 271 } 272 sto := tsto.sto 273 274 if opts.TargetPathFunc == nil && opts.TargetDir == "" { 275 pwd, err := os.Getwd() 276 if err != nil { 277 return "", nil, "", err 278 } 279 opts.TargetDir = pwd 280 } 281 282 if !opts.Revision.Unset() { 283 // XXX: is this really necessary (and, if it is, shoudn't we error out instead) 284 opts.Channel = "" 285 } 286 287 logger.Debugf("Going to download snap %q %s.", name, &opts) 288 289 actions := []*store.SnapAction{{ 290 Action: "download", 291 InstanceName: name, 292 Revision: opts.Revision, 293 CohortKey: opts.CohortKey, 294 Channel: opts.Channel, 295 }} 296 297 sars, _, err := sto.SnapAction(context.TODO(), nil, actions, nil, tsto.user, nil) 298 if err != nil { 299 // err will be 'cannot download snap "foo": <reasons>' 300 return "", nil, "", err 301 } 302 snap := sars[0].Info 303 redirectChannel = sars[0].RedirectChannel 304 305 if opts.TargetPathFunc == nil { 306 baseName := opts.Basename 307 if baseName == "" { 308 baseName = snap.Filename() 309 } else { 310 baseName += ".snap" 311 } 312 targetFn = filepath.Join(opts.TargetDir, baseName) 313 } else { 314 var err error 315 targetFn, err = opts.TargetPathFunc(snap) 316 if err != nil { 317 return "", nil, "", err 318 } 319 } 320 321 // check if we already have the right file 322 if osutil.FileExists(targetFn) { 323 sha3_384Dgst, size, err := osutil.FileDigest(targetFn, crypto.SHA3_384) 324 if err == nil && size == uint64(snap.DownloadInfo.Size) && fmt.Sprintf("%x", sha3_384Dgst) == snap.DownloadInfo.Sha3_384 { 325 logger.Debugf("not downloading, using existing file %s", targetFn) 326 return targetFn, snap, redirectChannel, nil 327 } 328 logger.Debugf("File exists but has wrong hash, ignoring (here).") 329 } 330 331 pb := progress.MakeProgressBar() 332 defer pb.Finished() 333 334 // Intercept sigint 335 c := make(chan os.Signal, 3) 336 signal.Notify(c, syscall.SIGINT) 337 go func() { 338 <-c 339 pb.Finished() 340 os.Exit(1) 341 }() 342 343 dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError} 344 if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user, dlOpts); err != nil { 345 return "", nil, "", err 346 } 347 348 signal.Reset(syscall.SIGINT) 349 350 return targetFn, snap, redirectChannel, nil 351 } 352 353 // AssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them. 354 func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher { 355 retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { 356 return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user) 357 } 358 save2 := func(a asserts.Assertion) error { 359 // for checking 360 err := db.Add(a) 361 if err != nil { 362 if _, ok := err.(*asserts.RevisionError); ok { 363 return nil 364 } 365 return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err) 366 } 367 return save(a) 368 } 369 return asserts.NewFetcher(db, retrieve, save2) 370 } 371 372 // FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database. 373 func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) (*asserts.SnapDeclaration, error) { 374 sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath) 375 if err != nil { 376 return nil, err 377 } 378 379 // this assumes series "16" 380 if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { 381 return nil, fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) 382 } 383 384 // cross checks 385 if err := snapasserts.CrossCheck(info.InstanceName(), sha3_384, size, &info.SideInfo, db); err != nil { 386 return nil, err 387 } 388 389 a, err := db.Find(asserts.SnapDeclarationType, map[string]string{ 390 "series": release.Series, 391 "snap-id": info.SnapID, 392 }) 393 if err != nil { 394 return nil, fmt.Errorf("internal error: lost snap declaration for %q: %v", info.InstanceName(), err) 395 } 396 return a.(*asserts.SnapDeclaration), nil 397 } 398 399 // Find provides the snapsserts.Finder interface for snapasserts.DerviceSideInfo 400 func (tsto *ToolingStore) Find(at *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) { 401 pk, err := asserts.PrimaryKeyFromHeaders(at, headers) 402 if err != nil { 403 return nil, err 404 } 405 return tsto.sto.Assertion(at, pk, tsto.user) 406 }