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  }