gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/daemon/api_sideload_n_try.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  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"mime/multipart"
    27  	"os"
    28  	"path/filepath"
    29  
    30  	"github.com/snapcore/snapd/asserts"
    31  	"github.com/snapcore/snapd/asserts/snapasserts"
    32  	"github.com/snapcore/snapd/client"
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/i18n"
    35  	"github.com/snapcore/snapd/osutil"
    36  	"github.com/snapcore/snapd/overlord/assertstate"
    37  	"github.com/snapcore/snapd/overlord/auth"
    38  	"github.com/snapcore/snapd/overlord/snapstate"
    39  	"github.com/snapcore/snapd/overlord/state"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/snap/snapfile"
    42  )
    43  
    44  const maxReadBuflen = 1024 * 1024
    45  
    46  func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string, user *auth.UserState) Response {
    47  	route := c.d.router.Get(stateChangeCmd.Path)
    48  	if route == nil {
    49  		return InternalError("cannot find route for change")
    50  	}
    51  
    52  	// POSTs to sideload snaps must be a multipart/form-data file upload.
    53  	form, err := multipart.NewReader(body, boundary).ReadForm(maxReadBuflen)
    54  	if err != nil {
    55  		return BadRequest("cannot read POST form: %v", err)
    56  	}
    57  
    58  	dangerousOK := isTrue(form, "dangerous")
    59  	flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic"))
    60  	if err != nil {
    61  		return BadRequest(err.Error())
    62  	}
    63  
    64  	if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" {
    65  		if len(form.Value["snap-path"]) == 0 {
    66  			return BadRequest("need 'snap-path' value in form")
    67  		}
    68  		return trySnap(c.d.overlord.State(), form.Value["snap-path"][0], flags)
    69  	}
    70  	flags.RemoveSnapPath = true
    71  
    72  	flags.Unaliased = isTrue(form, "unaliased")
    73  	flags.IgnoreRunning = isTrue(form, "ignore-running")
    74  
    75  	// find the file for the "snap" form field
    76  	var snapBody multipart.File
    77  	var origPath string
    78  out:
    79  	for name, fheaders := range form.File {
    80  		if name != "snap" {
    81  			continue
    82  		}
    83  		for _, fheader := range fheaders {
    84  			snapBody, err = fheader.Open()
    85  			origPath = fheader.Filename
    86  			if err != nil {
    87  				return BadRequest(`cannot open uploaded "snap" file: %v`, err)
    88  			}
    89  			defer snapBody.Close()
    90  
    91  			break out
    92  		}
    93  	}
    94  	defer form.RemoveAll()
    95  
    96  	if snapBody == nil {
    97  		return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`)
    98  	}
    99  
   100  	// we are in charge of the tempfile life cycle until we hand it off to the change
   101  	changeTriggered := false
   102  	// if you change this prefix, look for it in the tests
   103  	// also see localInstallCleanup in snapstate/snapmgr.go
   104  	tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix)
   105  	if err != nil {
   106  		return InternalError("cannot create temporary file: %v", err)
   107  	}
   108  
   109  	tempPath := tmpf.Name()
   110  
   111  	defer func() {
   112  		if !changeTriggered {
   113  			os.Remove(tempPath)
   114  		}
   115  	}()
   116  
   117  	if _, err := io.Copy(tmpf, snapBody); err != nil {
   118  		return InternalError("cannot copy request into temporary file: %v", err)
   119  	}
   120  	tmpf.Sync()
   121  
   122  	if len(form.Value["snap-path"]) > 0 {
   123  		origPath = form.Value["snap-path"][0]
   124  	}
   125  
   126  	var instanceName string
   127  
   128  	if len(form.Value["name"]) > 0 {
   129  		// caller has specified desired instance name
   130  		instanceName = form.Value["name"][0]
   131  		if err := snap.ValidateInstanceName(instanceName); err != nil {
   132  			return BadRequest(err.Error())
   133  		}
   134  	}
   135  
   136  	st := c.d.overlord.State()
   137  	st.Lock()
   138  	defer st.Unlock()
   139  
   140  	var snapName string
   141  	var sideInfo *snap.SideInfo
   142  
   143  	if !dangerousOK {
   144  		si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st))
   145  		switch {
   146  		case err == nil:
   147  			snapName = si.RealName
   148  			sideInfo = si
   149  		case asserts.IsNotFound(err):
   150  			// with devmode we try to find assertions but it's ok
   151  			// if they are not there (implies --dangerous)
   152  			if !isTrue(form, "devmode") {
   153  				msg := "cannot find signatures with metadata for snap"
   154  				if origPath != "" {
   155  					msg = fmt.Sprintf("%s %q", msg, origPath)
   156  				}
   157  				return BadRequest(msg)
   158  			}
   159  			// TODO: set a warning if devmode
   160  		default:
   161  			return BadRequest(err.Error())
   162  		}
   163  	}
   164  
   165  	if snapName == "" {
   166  		// potentially dangerous but dangerous or devmode params were set
   167  		info, err := unsafeReadSnapInfo(tempPath)
   168  		if err != nil {
   169  			return BadRequest("cannot read snap file: %v", err)
   170  		}
   171  		snapName = info.SnapName()
   172  		sideInfo = &snap.SideInfo{RealName: snapName}
   173  	}
   174  
   175  	if instanceName != "" {
   176  		requestedSnapName := snap.InstanceSnap(instanceName)
   177  		if requestedSnapName != snapName {
   178  			return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName))
   179  		}
   180  	} else {
   181  		instanceName = snapName
   182  	}
   183  
   184  	msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName)
   185  	if origPath != "" {
   186  		msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath)
   187  	}
   188  
   189  	tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags)
   190  	if err != nil {
   191  		return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v")
   192  	}
   193  
   194  	chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName})
   195  	chg.Set("api-data", map[string]string{"snap-name": instanceName})
   196  
   197  	ensureStateSoon(st)
   198  
   199  	// only when the unlock succeeds (as opposed to panicing) is the handoff done
   200  	// but this is good enough
   201  	changeTriggered = true
   202  
   203  	return AsyncResponse(nil, chg.ID())
   204  }
   205  
   206  func trySnap(st *state.State, trydir string, flags snapstate.Flags) Response {
   207  	st.Lock()
   208  	defer st.Unlock()
   209  
   210  	if !filepath.IsAbs(trydir) {
   211  		return BadRequest("cannot try %q: need an absolute path", trydir)
   212  	}
   213  	if !osutil.IsDirectory(trydir) {
   214  		return BadRequest("cannot try %q: not a snap directory", trydir)
   215  	}
   216  
   217  	// the developer asked us to do this with a trusted snap dir
   218  	info, err := unsafeReadSnapInfo(trydir)
   219  	if _, ok := err.(snap.NotSnapError); ok {
   220  		return &apiError{
   221  			Status:  400,
   222  			Message: err.Error(),
   223  			Kind:    client.ErrorKindNotSnap,
   224  		}
   225  	}
   226  	if err != nil {
   227  		return BadRequest("cannot read snap info for %s: %s", trydir, err)
   228  	}
   229  
   230  	tset, err := snapstateTryPath(st, info.InstanceName(), trydir, flags)
   231  	if err != nil {
   232  		return errToResponse(err, []string{info.InstanceName()}, BadRequest, "cannot try %s: %s", trydir)
   233  	}
   234  
   235  	msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.InstanceName(), trydir)
   236  	chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.InstanceName()})
   237  	chg.Set("api-data", map[string]string{"snap-name": info.InstanceName()})
   238  
   239  	ensureStateSoon(st)
   240  
   241  	return AsyncResponse(nil, chg.ID())
   242  }
   243  
   244  var unsafeReadSnapInfo = unsafeReadSnapInfoImpl
   245  
   246  func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) {
   247  	// Condider using DeriveSideInfo before falling back to this!
   248  	snapf, err := snapfile.Open(snapPath)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return snap.ReadInfoFromSnapFile(snapf, nil)
   253  }