github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-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 daemon
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"mime"
    30  	"mime/multipart"
    31  	"net/http"
    32  	"os"
    33  	"path/filepath"
    34  	"strconv"
    35  	"strings"
    36  
    37  	"github.com/gorilla/mux"
    38  
    39  	"github.com/snapcore/snapd/asserts"
    40  	"github.com/snapcore/snapd/asserts/snapasserts"
    41  	"github.com/snapcore/snapd/client"
    42  	"github.com/snapcore/snapd/dirs"
    43  	"github.com/snapcore/snapd/i18n"
    44  	"github.com/snapcore/snapd/logger"
    45  	"github.com/snapcore/snapd/osutil"
    46  	"github.com/snapcore/snapd/overlord/assertstate"
    47  	"github.com/snapcore/snapd/overlord/auth"
    48  	"github.com/snapcore/snapd/overlord/servicestate"
    49  	"github.com/snapcore/snapd/overlord/snapshotstate"
    50  	"github.com/snapcore/snapd/overlord/snapstate"
    51  	"github.com/snapcore/snapd/overlord/state"
    52  	"github.com/snapcore/snapd/progress"
    53  	"github.com/snapcore/snapd/sandbox"
    54  	"github.com/snapcore/snapd/snap"
    55  	"github.com/snapcore/snapd/snap/channel"
    56  	"github.com/snapcore/snapd/snap/snapfile"
    57  	"github.com/snapcore/snapd/strutil"
    58  )
    59  
    60  var api = []*Command{
    61  	rootCmd,
    62  	sysInfoCmd,
    63  	loginCmd,
    64  	logoutCmd,
    65  	appIconCmd,
    66  	findCmd,
    67  	snapsCmd,
    68  	snapCmd,
    69  	snapFileCmd,
    70  	snapDownloadCmd,
    71  	snapConfCmd,
    72  	interfacesCmd,
    73  	assertsCmd,
    74  	assertsFindManyCmd,
    75  	stateChangeCmd,
    76  	stateChangesCmd,
    77  	createUserCmd,
    78  	buyCmd,
    79  	readyToBuyCmd,
    80  	snapctlCmd,
    81  	usersCmd,
    82  	sectionsCmd,
    83  	aliasesCmd,
    84  	appsCmd,
    85  	logsCmd,
    86  	warningsCmd,
    87  	debugPprofCmd,
    88  	debugCmd,
    89  	snapshotCmd,
    90  	snapshotExportCmd,
    91  	connectionsCmd,
    92  	modelCmd,
    93  	cohortsCmd,
    94  	serialModelCmd,
    95  	systemsCmd,
    96  	systemsActionCmd,
    97  	validationSetsListCmd,
    98  	validationSetsCmd,
    99  	routineConsoleConfStartCmd,
   100  	systemRecoveryKeysCmd,
   101  }
   102  
   103  var (
   104  	// see daemon.go:canAccess for details how the access is controlled
   105  	snapsCmd = &Command{
   106  		Path:     "/v2/snaps",
   107  		UserOK:   true,
   108  		PolkitOK: "io.snapcraft.snapd.manage",
   109  		GET:      getSnapsInfo,
   110  		POST:     postSnaps,
   111  	}
   112  
   113  	snapCmd = &Command{
   114  		Path:     "/v2/snaps/{name}",
   115  		UserOK:   true,
   116  		PolkitOK: "io.snapcraft.snapd.manage",
   117  		GET:      getSnapInfo,
   118  		POST:     postSnap,
   119  	}
   120  )
   121  
   122  // UserFromRequest extracts user information from request and return the respective user in state, if valid
   123  // It requires the state to be locked
   124  func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) {
   125  	// extract macaroons data from request
   126  	header := req.Header.Get("Authorization")
   127  	if header == "" {
   128  		return nil, auth.ErrInvalidAuth
   129  	}
   130  
   131  	authorizationData := strings.SplitN(header, " ", 2)
   132  	if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" {
   133  		return nil, fmt.Errorf("authorization header misses Macaroon prefix")
   134  	}
   135  
   136  	var macaroon string
   137  	var discharges []string
   138  	for _, field := range strutil.CommaSeparatedList(authorizationData[1]) {
   139  		if strings.HasPrefix(field, `root="`) {
   140  			macaroon = strings.TrimSuffix(field[6:], `"`)
   141  		}
   142  		if strings.HasPrefix(field, `discharge="`) {
   143  			discharges = append(discharges, strings.TrimSuffix(field[11:], `"`))
   144  		}
   145  	}
   146  
   147  	if macaroon == "" {
   148  		return nil, fmt.Errorf("invalid authorization header")
   149  	}
   150  
   151  	user, err := auth.CheckMacaroon(st, macaroon, discharges)
   152  	return user, err
   153  }
   154  
   155  var muxVars = mux.Vars
   156  
   157  func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   158  	vars := muxVars(r)
   159  	name := vars["name"]
   160  
   161  	about, err := localSnapInfo(c.d.overlord.State(), name)
   162  	if err != nil {
   163  		if err == errNoSnap {
   164  			return SnapNotFound(name, err)
   165  		}
   166  
   167  		return InternalError("%v", err)
   168  	}
   169  
   170  	route := c.d.router.Get(c.Path)
   171  	if route == nil {
   172  		return InternalError("cannot find route for %q snap", name)
   173  	}
   174  
   175  	url, err := route.URL("name", name)
   176  	if err != nil {
   177  		return InternalError("cannot build URL for %q snap: %v", name, err)
   178  	}
   179  
   180  	sd := servicestate.NewStatusDecorator(progress.Null)
   181  
   182  	result := webify(mapLocal(about, sd), url.String())
   183  
   184  	return SyncResponse(result, nil)
   185  }
   186  
   187  func webify(result *client.Snap, resource string) *client.Snap {
   188  	if result.Icon == "" || strings.HasPrefix(result.Icon, "http") {
   189  		return result
   190  	}
   191  	result.Icon = ""
   192  
   193  	route := appIconCmd.d.router.Get(appIconCmd.Path)
   194  	if route != nil {
   195  		url, err := route.URL("name", result.Name)
   196  		if err == nil {
   197  			result.Icon = url.String()
   198  		}
   199  	}
   200  
   201  	return result
   202  }
   203  
   204  func getStore(c *Command) snapstate.StoreService {
   205  	st := c.d.overlord.State()
   206  	st.Lock()
   207  	defer st.Unlock()
   208  
   209  	return snapstate.Store(st, nil)
   210  }
   211  
   212  // plural!
   213  func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   214  
   215  	if shouldSearchStore(r) {
   216  		logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL)
   217  		return searchStore(c, r, user)
   218  	}
   219  
   220  	route := c.d.router.Get(snapCmd.Path)
   221  	if route == nil {
   222  		return InternalError("cannot find route for snaps")
   223  	}
   224  
   225  	query := r.URL.Query()
   226  	var all bool
   227  	sel := query.Get("select")
   228  	switch sel {
   229  	case "all":
   230  		all = true
   231  	case "enabled", "":
   232  		all = false
   233  	default:
   234  		return BadRequest("invalid select parameter: %q", sel)
   235  	}
   236  	var wanted map[string]bool
   237  	if ns := query.Get("snaps"); len(ns) > 0 {
   238  		nsl := strutil.CommaSeparatedList(ns)
   239  		wanted = make(map[string]bool, len(nsl))
   240  		for _, name := range nsl {
   241  			wanted[name] = true
   242  		}
   243  	}
   244  
   245  	found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted)
   246  	if err != nil {
   247  		return InternalError("cannot list local snaps! %v", err)
   248  	}
   249  
   250  	results := make([]*json.RawMessage, len(found))
   251  
   252  	sd := servicestate.NewStatusDecorator(progress.Null)
   253  	for i, x := range found {
   254  		name := x.info.InstanceName()
   255  		rev := x.info.Revision
   256  
   257  		url, err := route.URL("name", name)
   258  		if err != nil {
   259  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err)
   260  			continue
   261  		}
   262  
   263  		data, err := json.Marshal(webify(mapLocal(x, sd), url.String()))
   264  		if err != nil {
   265  			return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err)
   266  		}
   267  		raw := json.RawMessage(data)
   268  		results[i] = &raw
   269  	}
   270  
   271  	return SyncResponse(results, &Meta{Sources: []string{"local"}})
   272  }
   273  
   274  func shouldSearchStore(r *http.Request) bool {
   275  	// we should jump to the old behaviour iff q is given, or if
   276  	// sources is given and either empty or contains the word
   277  	// 'store'.  Otherwise, local results only.
   278  
   279  	query := r.URL.Query()
   280  
   281  	if _, ok := query["q"]; ok {
   282  		logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL)
   283  		return true
   284  	}
   285  
   286  	if src, ok := query["sources"]; ok {
   287  		logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL)
   288  		if len(src) == 0 || strings.Contains(src[0], "store") {
   289  			return true
   290  		}
   291  	}
   292  
   293  	return false
   294  }
   295  
   296  // licenseData holds details about the snap license, and may be
   297  // marshaled back as an error when the license agreement is pending,
   298  // and is expected as input to accept (or not) that license
   299  // agreement. As such, its field names are part of the API.
   300  type licenseData struct {
   301  	Intro   string `json:"intro"`
   302  	License string `json:"license"`
   303  	Agreed  bool   `json:"agreed"`
   304  }
   305  
   306  func (*licenseData) Error() string {
   307  	return "license agreement required"
   308  }
   309  
   310  type snapRevisionOptions struct {
   311  	Channel  string        `json:"channel"`
   312  	Revision snap.Revision `json:"revision"`
   313  
   314  	CohortKey   string `json:"cohort-key"`
   315  	LeaveCohort bool   `json:"leave-cohort"`
   316  }
   317  
   318  func (ropt *snapRevisionOptions) validate() error {
   319  	if ropt.CohortKey != "" {
   320  		if ropt.LeaveCohort {
   321  			return fmt.Errorf("cannot specify both cohort-key and leave-cohort")
   322  		}
   323  		if !ropt.Revision.Unset() {
   324  			return fmt.Errorf("cannot specify both cohort-key and revision")
   325  		}
   326  	}
   327  
   328  	if ropt.Channel != "" {
   329  		_, err := channel.Parse(ropt.Channel, "-")
   330  		if err != nil {
   331  			return err
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  type snapInstruction struct {
   338  	progress.NullMeter
   339  
   340  	Action string `json:"action"`
   341  	Amend  bool   `json:"amend"`
   342  	snapRevisionOptions
   343  	DevMode          bool `json:"devmode"`
   344  	JailMode         bool `json:"jailmode"`
   345  	Classic          bool `json:"classic"`
   346  	IgnoreValidation bool `json:"ignore-validation"`
   347  	IgnoreRunning    bool `json:"ignore-running"`
   348  	Unaliased        bool `json:"unaliased"`
   349  	Purge            bool `json:"purge,omitempty"`
   350  	// dropping support temporarely until flag confusion is sorted,
   351  	// this isn't supported by client atm anyway
   352  	LeaveOld bool         `json:"temp-dropped-leave-old"`
   353  	License  *licenseData `json:"license"`
   354  	Snaps    []string     `json:"snaps"`
   355  	Users    []string     `json:"users"`
   356  
   357  	// The fields below should not be unmarshalled into. Do not export them.
   358  	userID int
   359  	ctx    context.Context
   360  }
   361  
   362  func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions {
   363  	return &snapstate.RevisionOptions{
   364  		Channel:     inst.Channel,
   365  		Revision:    inst.Revision,
   366  		CohortKey:   inst.CohortKey,
   367  		LeaveCohort: inst.LeaveCohort,
   368  	}
   369  }
   370  
   371  func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) {
   372  	return modeFlags(inst.DevMode, inst.JailMode, inst.Classic)
   373  }
   374  
   375  func (inst *snapInstruction) installFlags() (snapstate.Flags, error) {
   376  	flags, err := inst.modeFlags()
   377  	if err != nil {
   378  		return snapstate.Flags{}, err
   379  	}
   380  	if inst.Unaliased {
   381  		flags.Unaliased = true
   382  	}
   383  	if inst.IgnoreRunning {
   384  		flags.IgnoreRunning = true
   385  	}
   386  
   387  	return flags, nil
   388  }
   389  
   390  func (inst *snapInstruction) validate() error {
   391  	if inst.CohortKey != "" {
   392  		if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" {
   393  			return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch")
   394  		}
   395  	}
   396  	if inst.LeaveCohort {
   397  		if inst.Action != "refresh" && inst.Action != "switch" {
   398  			return fmt.Errorf("leave-cohort can only be specified for refresh or switch")
   399  		}
   400  	}
   401  	if inst.Action == "install" {
   402  		for _, snapName := range inst.Snaps {
   403  			// FIXME: alternatively we could simply mutate *inst
   404  			//        and s/ubuntu-core/core/ ?
   405  			if snapName == "ubuntu-core" {
   406  				return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`)
   407  			}
   408  		}
   409  	}
   410  
   411  	return inst.snapRevisionOptions.validate()
   412  }
   413  
   414  type snapInstructionResult struct {
   415  	Summary  string
   416  	Affected []string
   417  	Tasksets []*state.TaskSet
   418  	Result   map[string]interface{}
   419  }
   420  
   421  var (
   422  	snapstateInstall           = snapstate.Install
   423  	snapstateInstallPath       = snapstate.InstallPath
   424  	snapstateRefreshCandidates = snapstate.RefreshCandidates
   425  	snapstateTryPath           = snapstate.TryPath
   426  	snapstateUpdate            = snapstate.Update
   427  	snapstateUpdateMany        = snapstate.UpdateMany
   428  	snapstateInstallMany       = snapstate.InstallMany
   429  	snapstateRemoveMany        = snapstate.RemoveMany
   430  	snapstateRevert            = snapstate.Revert
   431  	snapstateRevertToRevision  = snapstate.RevertToRevision
   432  	snapstateSwitch            = snapstate.Switch
   433  
   434  	snapshotList    = snapshotstate.List
   435  	snapshotCheck   = snapshotstate.Check
   436  	snapshotForget  = snapshotstate.Forget
   437  	snapshotRestore = snapshotstate.Restore
   438  	snapshotSave    = snapshotstate.Save
   439  	snapshotExport  = snapshotstate.Export
   440  	snapshotImport  = snapshotstate.Import
   441  
   442  	assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
   443  )
   444  
   445  func ensureStateSoonImpl(st *state.State) {
   446  	st.EnsureBefore(0)
   447  }
   448  
   449  var ensureStateSoon = ensureStateSoonImpl
   450  
   451  var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together")
   452  var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together")
   453  var errNoJailMode = errors.New("this system cannot honour the jailmode flag")
   454  
   455  func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) {
   456  	flags := snapstate.Flags{}
   457  	devModeOS := sandbox.ForceDevMode()
   458  	switch {
   459  	case jailMode && devModeOS:
   460  		return flags, errNoJailMode
   461  	case jailMode && devMode:
   462  		return flags, errDevJailModeConflict
   463  	case devMode && classic:
   464  		return flags, errClassicDevmodeConflict
   465  	}
   466  	// NOTE: jailmode and classic are allowed together. In that setting,
   467  	// jailmode overrides classic and the app gets regular (non-classic)
   468  	// confinement.
   469  	flags.JailMode = jailMode
   470  	flags.Classic = classic
   471  	flags.DevMode = devMode
   472  	return flags, nil
   473  }
   474  
   475  func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   476  	// we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly
   477  	if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   478  		return nil, err
   479  	}
   480  
   481  	// TODO: use a per-request context
   482  	updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	var msg string
   488  	switch len(updated) {
   489  	case 0:
   490  		if len(inst.Snaps) != 0 {
   491  			// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   492  			msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps))
   493  		} else {
   494  			msg = i18n.G("Refresh all snaps: no updates")
   495  		}
   496  	case 1:
   497  		msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
   498  	default:
   499  		quoted := strutil.Quoted(updated)
   500  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   501  		msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
   502  	}
   503  
   504  	return &snapInstructionResult{
   505  		Summary:  msg,
   506  		Affected: updated,
   507  		Tasksets: tasksets,
   508  	}, nil
   509  }
   510  
   511  func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   512  	for _, name := range inst.Snaps {
   513  		if len(name) == 0 {
   514  			return nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   515  		}
   516  	}
   517  	installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID)
   518  	if err != nil {
   519  		return nil, err
   520  	}
   521  
   522  	var msg string
   523  	switch len(inst.Snaps) {
   524  	case 0:
   525  		return nil, fmt.Errorf("cannot install zero snaps")
   526  	case 1:
   527  		msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0])
   528  	default:
   529  		quoted := strutil.Quoted(inst.Snaps)
   530  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   531  		msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted)
   532  	}
   533  
   534  	return &snapInstructionResult{
   535  		Summary:  msg,
   536  		Affected: installed,
   537  		Tasksets: tasksets,
   538  	}, nil
   539  }
   540  
   541  func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   542  	if len(inst.Snaps[0]) == 0 {
   543  		return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name"))
   544  	}
   545  
   546  	flags, err := inst.installFlags()
   547  	if err != nil {
   548  		return "", nil, err
   549  	}
   550  
   551  	var ckey string
   552  	if inst.CohortKey == "" {
   553  		logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
   554  	} else {
   555  		ckey = strutil.ElliptLeft(inst.CohortKey, 10)
   556  		logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
   557  	}
   558  	tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   559  	if err != nil {
   560  		return "", nil, err
   561  	}
   562  
   563  	msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0])
   564  	if inst.Channel != "stable" && inst.Channel != "" {
   565  		msg += fmt.Sprintf(" from %q channel", inst.Channel)
   566  	}
   567  	if inst.CohortKey != "" {
   568  		msg += fmt.Sprintf(" from %q cohort", ckey)
   569  	}
   570  	return msg, []*state.TaskSet{tset}, nil
   571  }
   572  
   573  func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   574  	// TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
   575  	flags, err := inst.modeFlags()
   576  	if err != nil {
   577  		return "", nil, err
   578  	}
   579  	if inst.IgnoreValidation {
   580  		flags.IgnoreValidation = true
   581  	}
   582  	if inst.IgnoreRunning {
   583  		flags.IgnoreRunning = true
   584  	}
   585  	if inst.Amend {
   586  		flags.Amend = true
   587  	}
   588  
   589  	// we need refreshed snap-declarations to enforce refresh-control as best as we can
   590  	if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
   591  		return "", nil, err
   592  	}
   593  
   594  	ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
   595  	if err != nil {
   596  		return "", nil, err
   597  	}
   598  
   599  	msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
   600  	if inst.Channel != "stable" && inst.Channel != "" {
   601  		msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
   602  	}
   603  
   604  	return msg, []*state.TaskSet{ts}, nil
   605  }
   606  
   607  func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   608  	removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps)
   609  	if err != nil {
   610  		return nil, err
   611  	}
   612  
   613  	var msg string
   614  	switch len(inst.Snaps) {
   615  	case 0:
   616  		return nil, fmt.Errorf("cannot remove zero snaps")
   617  	case 1:
   618  		msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0])
   619  	default:
   620  		quoted := strutil.Quoted(inst.Snaps)
   621  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   622  		msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted)
   623  	}
   624  
   625  	return &snapInstructionResult{
   626  		Summary:  msg,
   627  		Affected: removed,
   628  		Tasksets: tasksets,
   629  	}, nil
   630  }
   631  
   632  func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   633  	ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge})
   634  	if err != nil {
   635  		return "", nil, err
   636  	}
   637  
   638  	msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0])
   639  	return msg, []*state.TaskSet{ts}, nil
   640  }
   641  
   642  func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   643  	var ts *state.TaskSet
   644  
   645  	flags, err := inst.modeFlags()
   646  	if err != nil {
   647  		return "", nil, err
   648  	}
   649  
   650  	if inst.Revision.Unset() {
   651  		ts, err = snapstateRevert(st, inst.Snaps[0], flags)
   652  	} else {
   653  		ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
   654  	}
   655  	if err != nil {
   656  		return "", nil, err
   657  	}
   658  
   659  	msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0])
   660  	return msg, []*state.TaskSet{ts}, nil
   661  }
   662  
   663  func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   664  	if !inst.Revision.Unset() {
   665  		return "", nil, errors.New("enable takes no revision")
   666  	}
   667  	ts, err := snapstate.Enable(st, inst.Snaps[0])
   668  	if err != nil {
   669  		return "", nil, err
   670  	}
   671  
   672  	msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0])
   673  	return msg, []*state.TaskSet{ts}, nil
   674  }
   675  
   676  func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   677  	if !inst.Revision.Unset() {
   678  		return "", nil, errors.New("disable takes no revision")
   679  	}
   680  	ts, err := snapstate.Disable(st, inst.Snaps[0])
   681  	if err != nil {
   682  		return "", nil, err
   683  	}
   684  
   685  	msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0])
   686  	return msg, []*state.TaskSet{ts}, nil
   687  }
   688  
   689  func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
   690  	if !inst.Revision.Unset() {
   691  		return "", nil, errors.New("switch takes no revision")
   692  	}
   693  	ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts())
   694  	if err != nil {
   695  		return "", nil, err
   696  	}
   697  
   698  	var msg string
   699  	switch {
   700  	case inst.LeaveCohort && inst.Channel != "":
   701  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel)
   702  	case inst.LeaveCohort:
   703  		msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0])
   704  	case inst.CohortKey == "" && inst.Channel != "":
   705  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel)
   706  	case inst.CohortKey != "" && inst.Channel == "":
   707  		msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10))
   708  	default:
   709  		msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10))
   710  	}
   711  	return msg, []*state.TaskSet{ts}, nil
   712  }
   713  
   714  func snapshotMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
   715  	setID, snapshotted, ts, err := snapshotSave(st, inst.Snaps, inst.Users)
   716  	if err != nil {
   717  		return nil, err
   718  	}
   719  
   720  	var msg string
   721  	if len(inst.Snaps) == 0 {
   722  		msg = i18n.G("Snapshot all snaps")
   723  	} else {
   724  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   725  		msg = fmt.Sprintf(i18n.G("Snapshot snaps %s"), strutil.Quoted(inst.Snaps))
   726  	}
   727  
   728  	return &snapInstructionResult{
   729  		Summary:  msg,
   730  		Affected: snapshotted,
   731  		Tasksets: []*state.TaskSet{ts},
   732  		Result:   map[string]interface{}{"set-id": setID},
   733  	}, nil
   734  }
   735  
   736  type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error)
   737  type snapsActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error)
   738  
   739  var snapInstructionDispTable = map[string]snapActionFunc{
   740  	"install": snapInstall,
   741  	"refresh": snapUpdate,
   742  	"remove":  snapRemove,
   743  	"revert":  snapRevert,
   744  	"enable":  snapEnable,
   745  	"disable": snapDisable,
   746  	"switch":  snapSwitch,
   747  }
   748  
   749  func (inst *snapInstruction) dispatch() snapActionFunc {
   750  	if len(inst.Snaps) != 1 {
   751  		logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps))
   752  	}
   753  	return snapInstructionDispTable[inst.Action]
   754  }
   755  
   756  func (inst *snapInstruction) dispatchForMany() (op snapsActionFunc) {
   757  	switch inst.Action {
   758  	case "refresh":
   759  		op = snapUpdateMany
   760  	case "install":
   761  		op = snapInstallMany
   762  	case "remove":
   763  		op = snapRemoveMany
   764  	case "snapshot":
   765  		op = snapshotMany
   766  	}
   767  	return op
   768  }
   769  
   770  func (inst *snapInstruction) errToResponse(err error) Response {
   771  	if len(inst.Snaps) == 0 {
   772  		return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action)
   773  	}
   774  
   775  	return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps))
   776  }
   777  
   778  func postSnap(c *Command, r *http.Request, user *auth.UserState) Response {
   779  	route := c.d.router.Get(stateChangeCmd.Path)
   780  	if route == nil {
   781  		return InternalError("cannot find route for change")
   782  	}
   783  
   784  	decoder := json.NewDecoder(r.Body)
   785  	var inst snapInstruction
   786  	if err := decoder.Decode(&inst); err != nil {
   787  		return BadRequest("cannot decode request body into snap instruction: %v", err)
   788  	}
   789  	inst.ctx = r.Context()
   790  
   791  	state := c.d.overlord.State()
   792  	state.Lock()
   793  	defer state.Unlock()
   794  
   795  	if user != nil {
   796  		inst.userID = user.ID
   797  	}
   798  
   799  	vars := muxVars(r)
   800  	inst.Snaps = []string{vars["name"]}
   801  
   802  	if err := inst.validate(); err != nil {
   803  		return BadRequest("%s", err)
   804  	}
   805  
   806  	impl := inst.dispatch()
   807  	if impl == nil {
   808  		return BadRequest("unknown action %s", inst.Action)
   809  	}
   810  
   811  	msg, tsets, err := impl(&inst, state)
   812  	if err != nil {
   813  		return inst.errToResponse(err)
   814  	}
   815  
   816  	chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps)
   817  
   818  	ensureStateSoon(state)
   819  
   820  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
   821  }
   822  
   823  func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change {
   824  	chg := st.NewChange(kind, summary)
   825  	for _, ts := range tsets {
   826  		chg.AddAll(ts)
   827  	}
   828  	if snapNames != nil {
   829  		chg.Set("snap-names", snapNames)
   830  	}
   831  	return chg
   832  }
   833  
   834  const maxReadBuflen = 1024 * 1024
   835  
   836  func trySnap(st *state.State, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response {
   837  	st.Lock()
   838  	defer st.Unlock()
   839  
   840  	if !filepath.IsAbs(trydir) {
   841  		return BadRequest("cannot try %q: need an absolute path", trydir)
   842  	}
   843  	if !osutil.IsDirectory(trydir) {
   844  		return BadRequest("cannot try %q: not a snap directory", trydir)
   845  	}
   846  
   847  	// the developer asked us to do this with a trusted snap dir
   848  	info, err := unsafeReadSnapInfo(trydir)
   849  	if _, ok := err.(snap.NotSnapError); ok {
   850  		return SyncResponse(&resp{
   851  			Type: ResponseTypeError,
   852  			Result: &errorResult{
   853  				Message: err.Error(),
   854  				Kind:    client.ErrorKindNotSnap,
   855  			},
   856  			Status: 400,
   857  		}, nil)
   858  	}
   859  	if err != nil {
   860  		return BadRequest("cannot read snap info for %s: %s", trydir, err)
   861  	}
   862  
   863  	tset, err := snapstateTryPath(st, info.InstanceName(), trydir, flags)
   864  	if err != nil {
   865  		return errToResponse(err, []string{info.InstanceName()}, BadRequest, "cannot try %s: %s", trydir)
   866  	}
   867  
   868  	msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.InstanceName(), trydir)
   869  	chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.InstanceName()})
   870  	chg.Set("api-data", map[string]string{"snap-name": info.InstanceName()})
   871  
   872  	ensureStateSoon(st)
   873  
   874  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
   875  }
   876  
   877  func isTrue(form *multipart.Form, key string) bool {
   878  	value := form.Value[key]
   879  	if len(value) == 0 {
   880  		return false
   881  	}
   882  	b, err := strconv.ParseBool(value[0])
   883  	if err != nil {
   884  		return false
   885  	}
   886  
   887  	return b
   888  }
   889  
   890  func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response {
   891  	route := c.d.router.Get(stateChangeCmd.Path)
   892  	if route == nil {
   893  		return InternalError("cannot find route for change")
   894  	}
   895  
   896  	decoder := json.NewDecoder(r.Body)
   897  	var inst snapInstruction
   898  	if err := decoder.Decode(&inst); err != nil {
   899  		return BadRequest("cannot decode request body into snap instruction: %v", err)
   900  	}
   901  
   902  	// TODO: inst.Amend, etc?
   903  	if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge {
   904  		return BadRequest("unsupported option provided for multi-snap operation")
   905  	}
   906  	if err := inst.validate(); err != nil {
   907  		return BadRequest("%v", err)
   908  	}
   909  
   910  	st := c.d.overlord.State()
   911  	st.Lock()
   912  	defer st.Unlock()
   913  
   914  	if user != nil {
   915  		inst.userID = user.ID
   916  	}
   917  
   918  	op := inst.dispatchForMany()
   919  	if op == nil {
   920  		return BadRequest("unsupported multi-snap operation %q", inst.Action)
   921  	}
   922  	res, err := op(&inst, st)
   923  	if err != nil {
   924  		return inst.errToResponse(err)
   925  	}
   926  
   927  	var chg *state.Change
   928  	if len(res.Tasksets) == 0 {
   929  		chg = st.NewChange(inst.Action+"-snap", res.Summary)
   930  		chg.SetStatus(state.DoneStatus)
   931  	} else {
   932  		chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected)
   933  		ensureStateSoon(st)
   934  	}
   935  
   936  	chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected})
   937  
   938  	return AsyncResponse(res.Result, &Meta{Change: chg.ID()})
   939  }
   940  
   941  func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
   942  	contentType := r.Header.Get("Content-Type")
   943  
   944  	mediaType, params, err := mime.ParseMediaType(contentType)
   945  	if err != nil {
   946  		return BadRequest("cannot parse content type: %v", err)
   947  	}
   948  
   949  	if mediaType == "application/json" {
   950  		charset := strings.ToUpper(params["charset"])
   951  		if charset != "" && charset != "UTF-8" {
   952  			return BadRequest("unknown charset in content type: %s", contentType)
   953  		}
   954  		return snapsOp(c, r, user)
   955  	}
   956  
   957  	if !strings.HasPrefix(contentType, "multipart/") {
   958  		return BadRequest("unknown content type: %s", contentType)
   959  	}
   960  
   961  	route := c.d.router.Get(stateChangeCmd.Path)
   962  	if route == nil {
   963  		return InternalError("cannot find route for change")
   964  	}
   965  
   966  	// POSTs to sideload snaps must be a multipart/form-data file upload.
   967  	form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen)
   968  	if err != nil {
   969  		return BadRequest("cannot read POST form: %v", err)
   970  	}
   971  
   972  	dangerousOK := isTrue(form, "dangerous")
   973  	flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic"))
   974  	if err != nil {
   975  		return BadRequest(err.Error())
   976  	}
   977  
   978  	if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" {
   979  		if len(form.Value["snap-path"]) == 0 {
   980  			return BadRequest("need 'snap-path' value in form")
   981  		}
   982  		return trySnap(c.d.overlord.State(), r, user, form.Value["snap-path"][0], flags)
   983  	}
   984  	flags.RemoveSnapPath = true
   985  
   986  	flags.Unaliased = isTrue(form, "unaliased")
   987  	flags.IgnoreRunning = isTrue(form, "ignore-running")
   988  
   989  	// find the file for the "snap" form field
   990  	var snapBody multipart.File
   991  	var origPath string
   992  out:
   993  	for name, fheaders := range form.File {
   994  		if name != "snap" {
   995  			continue
   996  		}
   997  		for _, fheader := range fheaders {
   998  			snapBody, err = fheader.Open()
   999  			origPath = fheader.Filename
  1000  			if err != nil {
  1001  				return BadRequest(`cannot open uploaded "snap" file: %v`, err)
  1002  			}
  1003  			defer snapBody.Close()
  1004  
  1005  			break out
  1006  		}
  1007  	}
  1008  	defer form.RemoveAll()
  1009  
  1010  	if snapBody == nil {
  1011  		return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`)
  1012  	}
  1013  
  1014  	// we are in charge of the tempfile life cycle until we hand it off to the change
  1015  	changeTriggered := false
  1016  	// if you change this prefix, look for it in the tests
  1017  	// also see localInstallCleanup in snapstate/snapmgr.go
  1018  	tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix)
  1019  	if err != nil {
  1020  		return InternalError("cannot create temporary file: %v", err)
  1021  	}
  1022  
  1023  	tempPath := tmpf.Name()
  1024  
  1025  	defer func() {
  1026  		if !changeTriggered {
  1027  			os.Remove(tempPath)
  1028  		}
  1029  	}()
  1030  
  1031  	if _, err := io.Copy(tmpf, snapBody); err != nil {
  1032  		return InternalError("cannot copy request into temporary file: %v", err)
  1033  	}
  1034  	tmpf.Sync()
  1035  
  1036  	if len(form.Value["snap-path"]) > 0 {
  1037  		origPath = form.Value["snap-path"][0]
  1038  	}
  1039  
  1040  	var instanceName string
  1041  
  1042  	if len(form.Value["name"]) > 0 {
  1043  		// caller has specified desired instance name
  1044  		instanceName = form.Value["name"][0]
  1045  		if err := snap.ValidateInstanceName(instanceName); err != nil {
  1046  			return BadRequest(err.Error())
  1047  		}
  1048  	}
  1049  
  1050  	st := c.d.overlord.State()
  1051  	st.Lock()
  1052  	defer st.Unlock()
  1053  
  1054  	var snapName string
  1055  	var sideInfo *snap.SideInfo
  1056  
  1057  	if !dangerousOK {
  1058  		si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st))
  1059  		switch {
  1060  		case err == nil:
  1061  			snapName = si.RealName
  1062  			sideInfo = si
  1063  		case asserts.IsNotFound(err):
  1064  			// with devmode we try to find assertions but it's ok
  1065  			// if they are not there (implies --dangerous)
  1066  			if !isTrue(form, "devmode") {
  1067  				msg := "cannot find signatures with metadata for snap"
  1068  				if origPath != "" {
  1069  					msg = fmt.Sprintf("%s %q", msg, origPath)
  1070  				}
  1071  				return BadRequest(msg)
  1072  			}
  1073  			// TODO: set a warning if devmode
  1074  		default:
  1075  			return BadRequest(err.Error())
  1076  		}
  1077  	}
  1078  
  1079  	if snapName == "" {
  1080  		// potentially dangerous but dangerous or devmode params were set
  1081  		info, err := unsafeReadSnapInfo(tempPath)
  1082  		if err != nil {
  1083  			return BadRequest("cannot read snap file: %v", err)
  1084  		}
  1085  		snapName = info.SnapName()
  1086  		sideInfo = &snap.SideInfo{RealName: snapName}
  1087  	}
  1088  
  1089  	if instanceName != "" {
  1090  		requestedSnapName := snap.InstanceSnap(instanceName)
  1091  		if requestedSnapName != snapName {
  1092  			return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName))
  1093  		}
  1094  	} else {
  1095  		instanceName = snapName
  1096  	}
  1097  
  1098  	msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName)
  1099  	if origPath != "" {
  1100  		msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath)
  1101  	}
  1102  
  1103  	tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags)
  1104  	if err != nil {
  1105  		return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v")
  1106  	}
  1107  
  1108  	chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName})
  1109  	chg.Set("api-data", map[string]string{"snap-name": instanceName})
  1110  
  1111  	ensureStateSoon(st)
  1112  
  1113  	// only when the unlock succeeds (as opposed to panicing) is the handoff done
  1114  	// but this is good enough
  1115  	changeTriggered = true
  1116  
  1117  	return AsyncResponse(nil, &Meta{Change: chg.ID()})
  1118  }
  1119  
  1120  func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) {
  1121  	// Condider using DeriveSideInfo before falling back to this!
  1122  	snapf, err := snapfile.Open(snapPath)
  1123  	if err != nil {
  1124  		return nil, err
  1125  	}
  1126  	return snap.ReadInfoFromSnapFile(snapf, nil)
  1127  }
  1128  
  1129  var unsafeReadSnapInfo = unsafeReadSnapInfoImpl